installer: move bitcoind steps to own module

This commit is contained in:
jp1ac4 2023-08-24 10:54:20 +01:00
parent cf17cc2cd6
commit bc35cdaad8
No known key found for this signature in database
GPG Key ID: A7ACD32423568D7B
2 changed files with 779 additions and 768 deletions

View File

@ -0,0 +1,770 @@
use std::collections::BTreeMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use iced::Command;
use liana::{config::BitcoindConfig, miniscript::bitcoin::Network};
use tracing::info;
use jsonrpc::{client::Client, simple_http::SimpleHttpTransport};
use liana_ui::{component::form, widget::*};
use crate::{
bitcoind::{start_internal_bitcoind, stop_internal_bitcoind, StartInternalBitcoindError},
installer::{
context::Context,
internal_bitcoind_datadir,
message::{self, Message},
step::Step,
view, Error, InternalBitcoindExeConfig,
},
utils::poll_for_file,
};
pub struct DefineBitcoind {
cookie_path: form::Value<String>,
address: form::Value<String>,
is_running: Option<Result<(), Error>>,
}
pub struct InternalBitcoindStep {
bitcoind_datadir: PathBuf,
network: Network,
started: Option<Result<(), StartInternalBitcoindError>>,
exe_path: Option<PathBuf>,
bitcoind_config: Option<BitcoindConfig>,
exe_config: Option<InternalBitcoindExeConfig>,
internal_bitcoind_config: Option<InternalBitcoindConfig>,
error: Option<String>,
}
pub struct SelectBitcoindTypeStep {
use_external: bool,
}
/// Default prune value used by internal bitcoind.
pub const PRUNE_DEFAULT: u32 = 15_000;
/// Default ports used by bitcoind across all networks.
pub const BITCOIND_DEFAULT_PORTS: [u16; 8] = [8332, 8333, 18332, 18333, 18443, 18444, 38332, 38333];
/// Represents section for a single network in `bitcoin.conf` file.
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct InternalBitcoindNetworkConfig {
rpc_port: u16,
p2p_port: u16,
prune: u32,
}
/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
#[derive(Debug, Clone)]
pub struct InternalBitcoindConfig {
networks: BTreeMap<Network, InternalBitcoindNetworkConfig>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum InternalBitcoindConfigError {
KeyNotFound(String),
CouldNotParseValue(String),
UnexpectedSection(String),
TooManyElements(String),
FileNotFound,
ReadingFile(String),
WritingFile(String),
Unexpected(String),
}
impl std::fmt::Display for InternalBitcoindConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e),
Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e),
Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e),
Self::TooManyElements(section) => {
write!(f, "Section in file contains too many elements: {}", section)
}
Self::FileNotFound => write!(f, "File not found"),
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
}
impl Default for InternalBitcoindConfig {
fn default() -> Self {
Self::new()
}
}
impl InternalBitcoindConfig {
pub fn new() -> Self {
Self {
networks: BTreeMap::new(),
}
}
pub fn from_ini(ini: &ini::Ini) -> Result<Self, InternalBitcoindConfigError> {
let mut networks = BTreeMap::new();
for (maybe_sec, prop) in ini {
if let Some(sec) = maybe_sec {
let network = Network::from_core_arg(sec)
.map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?;
if prop.len() > 3 {
return Err(InternalBitcoindConfigError::TooManyElements(
sec.to_string(),
));
}
let rpc_port = prop
.get("rpcport")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))?
.parse::<u16>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
let p2p_port = prop
.get("port")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
.parse::<u16>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
let prune = prop
.get("prune")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
.parse::<u32>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
networks.insert(
network,
InternalBitcoindNetworkConfig {
rpc_port,
p2p_port,
prune,
},
);
} else if !prop.is_empty() {
return Err(InternalBitcoindConfigError::UnexpectedSection(
"General section should be empty".to_string(),
));
}
}
Ok(Self { networks })
}
pub fn from_file(path: &PathBuf) -> Result<Self, InternalBitcoindConfigError> {
if !path.exists() {
return Err(InternalBitcoindConfigError::FileNotFound);
}
let conf_ini = ini::Ini::load_from_file(path)
.map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?;
Self::from_ini(&conf_ini)
}
pub fn to_ini(&self) -> ini::Ini {
let mut conf_ini = ini::Ini::new();
for (network, network_conf) in &self.networks {
conf_ini
.with_section(Some(network.to_core_arg()))
.set("rpcport", network_conf.rpc_port.to_string())
.set("port", network_conf.p2p_port.to_string())
.set("prune", network_conf.prune.to_string());
}
conf_ini
}
pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> {
std::fs::create_dir_all(
path.parent()
.ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?,
)
.map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?;
info!("Writing to file {}", path.to_string_lossy());
self.to_ini()
.write_to_file(path)
.map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?;
Ok(())
}
}
/// Path of the `bitcoin.conf` file used by internal bitcoind.
fn internal_bitcoind_config_path(bitcoind_datadir: &PathBuf) -> PathBuf {
let mut config_path = PathBuf::from(bitcoind_datadir);
config_path.push("bitcoin.conf");
config_path
}
/// Path of the cookie file used by internal bitcoind on a given network.
fn internal_bitcoind_cookie_path(bitcoind_datadir: &Path, network: &Network) -> PathBuf {
let mut cookie_path = bitcoind_datadir.to_path_buf();
if let Some(dir) = bitcoind_network_dir(network) {
cookie_path.push(dir);
}
cookie_path.push(".cookie");
cookie_path
}
/// RPC address for internal bitcoind.
fn internal_bitcoind_address(rpc_port: u16) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_port)
}
fn bitcoind_default_datadir() -> Option<PathBuf> {
#[cfg(target_os = "linux")]
let configs_dir = dirs::home_dir();
#[cfg(not(target_os = "linux"))]
let configs_dir = dirs::config_dir();
if let Some(mut path) = configs_dir {
#[cfg(target_os = "linux")]
path.push(".bitcoin");
#[cfg(not(target_os = "linux"))]
path.push("Bitcoin");
return Some(path);
}
None
}
fn bitcoind_network_dir(network: &Network) -> Option<String> {
let dir = match network {
Network::Bitcoin => {
return None;
}
Network::Testnet => "testnet3",
Network::Regtest => "regtest",
Network::Signet => "signet",
_ => panic!("Directory required for this network is unknown."),
};
Some(dir.to_string())
}
fn bitcoind_default_cookie_path(network: &Network) -> Option<String> {
if let Some(mut path) = bitcoind_default_datadir() {
if let Some(dir) = bitcoind_network_dir(network) {
path.push(dir);
}
path.push(".cookie");
return path.to_str().map(|s| s.to_string());
}
None
}
fn bitcoind_default_address(network: &Network) -> String {
match network {
Network::Bitcoin => "127.0.0.1:8332".to_string(),
Network::Testnet => "127.0.0.1:18332".to_string(),
Network::Regtest => "127.0.0.1:18443".to_string(),
Network::Signet => "127.0.0.1:38332".to_string(),
_ => "127.0.0.1:8332".to_string(),
}
}
/// Looks for bitcoind executable path and returns `None` if not found.
fn bitcoind_exe_path() -> Option<PathBuf> {
which::which("bitcoind").ok()
}
/// Get available port that is valid for use by internal bitcoind.
// Modified from https://github.com/RCasatta/bitcoind/blob/f047740d7d0af935ff7360cf77429c5f294cfd59/src/lib.rs#L435
pub fn get_available_port() -> Result<u16, Error> {
// Perform multiple attempts to get a valid port.
for _ in 0..10 {
// Using 0 as port lets the system assign a port available.
let t = TcpListener::bind(("127.0.0.1", 0))
.map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?;
let port = t
.local_addr()
.map(|s| s.port())
.map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?;
if port_is_valid(&port) {
return Ok(port);
}
}
Err(Error::CannotGetAvailablePort(
"Exhausted attempts".to_string(),
))
}
/// Checks if port is valid for use by internal bitcoind.
pub fn port_is_valid(port: &u16) -> bool {
!BITCOIND_DEFAULT_PORTS.contains(port)
}
impl Default for SelectBitcoindTypeStep {
fn default() -> Self {
Self::new()
}
}
impl From<SelectBitcoindTypeStep> for Box<dyn Step> {
fn from(s: SelectBitcoindTypeStep) -> Box<dyn Step> {
Box::new(s)
}
}
impl SelectBitcoindTypeStep {
pub fn new() -> Self {
Self { use_external: true }
}
}
impl Step for SelectBitcoindTypeStep {
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::SelectBitcoindType(msg) = message {
match msg {
message::SelectBitcoindTypeMsg::UseExternal(selected) => {
self.use_external = selected;
}
};
return Command::perform(async {}, |_| Message::Next);
};
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
if !self.use_external {
if ctx.internal_bitcoind_config.is_none() {
ctx.bitcoind_config = None; // Ensures internal bitcoind can be restarted in case user has switched selection.
}
} else {
ctx.internal_bitcoind_config = None;
ctx.internal_bitcoind_exe_config = None;
}
ctx.bitcoind_is_external = self.use_external;
true
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::select_bitcoind_type(progress)
}
}
impl DefineBitcoind {
pub fn new() -> Self {
Self {
cookie_path: form::Value::default(),
address: form::Value::default(),
is_running: None,
}
}
pub fn ping(&self) -> Command<Message> {
let address = self.address.value.to_owned();
let cookie_path = self.cookie_path.value.to_owned();
Command::perform(
async move {
let cookie = std::fs::read_to_string(&cookie_path)
.map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?;
let client = Client::with_transport(
SimpleHttpTransport::builder()
.url(&address)?
.timeout(std::time::Duration::from_secs(3))
.cookie_auth(cookie)
.build(),
);
client.send_request(client.build_request("echo", &[]))?;
Ok(())
},
|res| Message::DefineBitcoind(message::DefineBitcoind::PingBitcoindResult(res)),
)
}
}
impl Step for DefineBitcoind {
fn load_context(&mut self, ctx: &Context) {
if self.cookie_path.value.is_empty() {
self.cookie_path.value =
bitcoind_default_cookie_path(&ctx.bitcoin_config.network).unwrap_or_default()
}
if self.address.value.is_empty() {
self.address.value = bitcoind_default_address(&ctx.bitcoin_config.network);
}
}
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::DefineBitcoind(msg) = message {
match msg {
message::DefineBitcoind::PingBitcoind => {
self.is_running = None;
return self.ping();
}
message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res),
message::DefineBitcoind::AddressEdited(address) => {
self.is_running = None;
self.address.value = address;
self.address.valid = true;
}
message::DefineBitcoind::CookiePathEdited(path) => {
self.is_running = None;
self.cookie_path.value = path;
self.address.valid = true;
}
};
};
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
match (
PathBuf::from_str(&self.cookie_path.value),
std::net::SocketAddr::from_str(&self.address.value),
) {
(Err(_), Ok(_)) => {
self.cookie_path.valid = false;
false
}
(Ok(_), Err(_)) => {
self.address.valid = false;
false
}
(Err(_), Err(_)) => {
self.cookie_path.valid = false;
self.address.valid = false;
false
}
(Ok(path), Ok(addr)) => {
ctx.bitcoind_config = Some(BitcoindConfig {
cookie_path: path,
addr,
});
true
}
}
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::define_bitcoin(
progress,
&self.address,
&self.cookie_path,
self.is_running.as_ref(),
)
}
fn load(&self) -> Command<Message> {
self.ping()
}
fn skip(&self, ctx: &Context) -> bool {
!ctx.bitcoind_is_external
}
}
impl Default for DefineBitcoind {
fn default() -> Self {
Self::new()
}
}
impl From<DefineBitcoind> for Box<dyn Step> {
fn from(s: DefineBitcoind) -> Box<dyn Step> {
Box::new(s)
}
}
impl From<InternalBitcoindStep> for Box<dyn Step> {
fn from(s: InternalBitcoindStep) -> Box<dyn Step> {
Box::new(s)
}
}
impl InternalBitcoindStep {
pub fn new(liana_datadir: &PathBuf) -> Self {
Self {
bitcoind_datadir: internal_bitcoind_datadir(liana_datadir),
network: Network::Bitcoin,
started: None,
exe_path: None,
bitcoind_config: None,
exe_config: None,
internal_bitcoind_config: None,
error: None,
}
}
}
impl Step for InternalBitcoindStep {
fn load_context(&mut self, ctx: &Context) {
if self.exe_path.is_none() {
self.exe_path = if let Some(exe_config) = ctx.internal_bitcoind_exe_config.clone() {
Some(exe_config.exe_path)
} else {
bitcoind_exe_path()
};
}
self.network = ctx.bitcoin_config.network;
if let Some(Ok(_)) = self.started {
// This case can arise if a user switches from internal bitcoind to external and back to internal.
if ctx.bitcoind_config.is_none() {
self.started = None; // So that internal bitcoind will be restarted.
}
}
}
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::InternalBitcoind(msg) = message {
match msg {
message::InternalBitcoindMsg::Previous => {
if self.internal_bitcoind_config.is_some() {
if let Some(bitcoind_config) = &self.bitcoind_config {
stop_internal_bitcoind(bitcoind_config);
}
}
return Command::perform(async {}, |_| Message::Previous);
}
message::InternalBitcoindMsg::Reload => {
return self.load();
}
message::InternalBitcoindMsg::DefineConfig => {
let mut conf = match InternalBitcoindConfig::from_file(
&internal_bitcoind_config_path(&self.bitcoind_datadir),
) {
Ok(conf) => conf,
Err(InternalBitcoindConfigError::FileNotFound) => {
InternalBitcoindConfig::new()
}
Err(e) => {
self.error = Some(e.to_string());
return Command::none();
}
};
// Insert entry for network if not present.
if conf.networks.get(&self.network).is_none() {
let network_conf = match (get_available_port(), get_available_port()) {
(Ok(rpc_port), Ok(p2p_port)) => {
// In case ports are the same, user will need to click button again for another attempt.
if rpc_port == p2p_port {
self.error = Some(
"Could not get distinct ports. Please try again."
.to_string(),
);
return Command::none();
}
InternalBitcoindNetworkConfig {
rpc_port,
p2p_port,
prune: PRUNE_DEFAULT,
}
}
(Ok(_), Err(e)) | (Err(e), Ok(_)) => {
self.error = Some(format!("Could not get available port: {}.", e));
return Command::none();
}
(Err(e1), Err(e2)) => {
self.error =
Some(format!("Could not get available ports: {}; {}.", e1, e2));
return Command::none();
}
};
conf.networks.insert(self.network, network_conf);
}
if let Err(e) =
conf.to_file(&internal_bitcoind_config_path(&self.bitcoind_datadir))
{
self.error = Some(e.to_string());
return Command::none();
};
self.error = None;
self.internal_bitcoind_config = Some(conf.clone());
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::Reload)
});
}
message::InternalBitcoindMsg::Start => {
if let Some(path) = &self.exe_path {
let datadir = match self.bitcoind_datadir.canonicalize() {
Ok(datadir) => datadir,
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::CouldNotCanonicalizeDataDir(
e.to_string(),
),
));
return Command::none();
}
};
let exe_config = InternalBitcoindExeConfig {
exe_path: path.to_path_buf(),
data_dir: datadir,
};
if let Err(e) = start_internal_bitcoind(&self.network, exe_config.clone()) {
self.started =
Some(Err(StartInternalBitcoindError::CommandError(e.to_string())));
return Command::none();
}
// Need to wait for cookie file to appear.
let cookie_path =
internal_bitcoind_cookie_path(&self.bitcoind_datadir, &self.network);
if !poll_for_file(&cookie_path, 200, 15) {
self.started =
Some(Err(StartInternalBitcoindError::CookieFileNotFound(
cookie_path.to_string_lossy().into_owned(),
)));
return Command::none();
}
let rpc_port = self
.internal_bitcoind_config
.as_ref()
.expect("Already added")
.clone()
.networks
.get(&self.network)
.expect("Already added")
.rpc_port;
let bitcoind_config = match cookie_path.canonicalize() {
Ok(cookie_path) => BitcoindConfig {
cookie_path,
addr: internal_bitcoind_address(rpc_port),
},
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(
e.to_string(),
),
));
return Command::none();
}
};
match liana::BitcoinD::new(
&bitcoind_config,
"internal_bitcoind_connection_check".to_string(),
) {
Ok(_) => {
self.error = None;
self.bitcoind_config = Some(bitcoind_config);
self.exe_config = Some(exe_config);
self.started = Some(Ok(()));
}
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::BitcoinDError(e.to_string()),
));
}
}
}
}
};
};
Command::none()
}
fn load(&self) -> Command<Message> {
if self.internal_bitcoind_config.is_none() {
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::DefineConfig)
});
}
if self.started.is_none() {
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::Start)
});
}
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
// Any errors have been handled as part of `message::InternalBitcoindMsg::Start`
if let Some(Ok(_)) = self.started {
ctx.bitcoind_config = self.bitcoind_config.clone();
ctx.internal_bitcoind_config = self.internal_bitcoind_config.clone();
ctx.internal_bitcoind_exe_config = self.exe_config.clone();
self.error = None;
return true;
}
false
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::start_internal_bitcoind(
progress,
self.exe_path.as_ref(),
self.started.as_ref(),
self.error.as_ref(),
)
}
fn stop(&self) {
// In case the installer is closed before changes written to context, stop bitcoind.
if let Some(Ok(_)) = self.started {
if let Some(bitcoind_config) = &self.bitcoind_config {
stop_internal_bitcoind(bitcoind_config);
}
}
}
fn skip(&self, ctx: &Context) -> bool {
ctx.bitcoind_is_external
}
}
#[cfg(test)]
mod tests {
use crate::installer::step::bitcoind::{InternalBitcoindConfig, InternalBitcoindNetworkConfig};
use ini::Ini;
use liana::miniscript::bitcoin::Network;
// Test the format of the internal bitcoind configuration file.
#[test]
fn internal_bitcoind_config() {
// A valid config
let mut conf_ini = Ini::new();
conf_ini
.with_section(Some("main"))
.set("rpcport", "43345")
.set("port", "42355")
.set("prune", "15246");
conf_ini
.with_section(Some("regtest"))
.set("rpcport", "34067")
.set("port", "45175")
.set("prune", "2043");
let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
let main_conf = InternalBitcoindNetworkConfig {
rpc_port: 43345,
p2p_port: 42355,
prune: 15246,
};
let regtest_conf = InternalBitcoindNetworkConfig {
rpc_port: 34067,
p2p_port: 45175,
prune: 2043,
};
assert_eq!(conf.networks.len(), 2);
assert_eq!(
conf.networks.get(&Network::Bitcoin).expect("Missing main"),
&main_conf
);
assert_eq!(
conf.networks
.get(&Network::Regtest)
.expect("Missing regtest"),
&regtest_conf
);
let mut conf = InternalBitcoindConfig::new();
conf.networks.insert(Network::Bitcoin, main_conf);
conf.networks.insert(Network::Regtest, regtest_conf);
for (sec, prop) in &conf.to_ini() {
if let Some(sec) = sec {
assert_eq!(prop.len(), 3);
let rpc_port = prop.get("rpcport").expect("rpcport");
let p2p_port = prop.get("port").expect("port");
let prune = prop.get("prune").expect("prune");
if sec == "main" {
assert_eq!(rpc_port, "43345");
assert_eq!(p2p_port, "42355");
assert_eq!(prune, "15246");
} else if sec == "regtest" {
assert_eq!(rpc_port, "34067");
assert_eq!(p2p_port, "45175");
assert_eq!(prune, "2043");
} else {
panic!("Unexpected section");
}
} else {
assert!(prop.is_empty())
}
}
}
}

View File

@ -1,39 +1,25 @@
mod bitcoind;
mod descriptor;
mod mnemonic;
pub use bitcoind::{
DefineBitcoind, InternalBitcoindConfig, InternalBitcoindStep, SelectBitcoindTypeStep,
};
pub use descriptor::{
BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor,
};
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
use std::collections::BTreeMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::path::PathBuf;
use iced::Command;
use liana::{
config::BitcoindConfig,
miniscript::bitcoin::{bip32::Fingerprint, Network},
};
use liana::miniscript::bitcoin::bip32::Fingerprint;
use tracing::info;
use liana_ui::widget::*;
use jsonrpc::{client::Client, simple_http::SimpleHttpTransport};
use liana_ui::{component::form, widget::*};
use crate::{
bitcoind::{start_internal_bitcoind, stop_internal_bitcoind, StartInternalBitcoindError},
installer::{
context::Context,
internal_bitcoind_datadir,
message::{self, Message},
view, Error, InternalBitcoindExeConfig,
},
utils::poll_for_file,
};
use crate::installer::{context::Context, message::Message, view};
pub trait Step {
fn update(&mut self, _message: Message) -> Command<Message> {
@ -68,680 +54,6 @@ impl From<Welcome> for Box<dyn Step> {
}
}
pub struct DefineBitcoind {
cookie_path: form::Value<String>,
address: form::Value<String>,
is_running: Option<Result<(), Error>>,
}
pub struct InternalBitcoindStep {
bitcoind_datadir: PathBuf,
network: Network,
started: Option<Result<(), StartInternalBitcoindError>>,
exe_path: Option<PathBuf>,
bitcoind_config: Option<BitcoindConfig>,
exe_config: Option<InternalBitcoindExeConfig>,
internal_bitcoind_config: Option<InternalBitcoindConfig>,
error: Option<String>,
}
pub struct SelectBitcoindTypeStep {
use_external: bool,
}
/// Default prune value used by internal bitcoind.
pub const PRUNE_DEFAULT: u32 = 15_000;
/// Default ports used by bitcoind across all networks.
pub const BITCOIND_DEFAULT_PORTS: [u16; 8] = [8332, 8333, 18332, 18333, 18443, 18444, 38332, 38333];
/// Represents section for a single network in `bitcoin.conf` file.
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct InternalBitcoindNetworkConfig {
rpc_port: u16,
p2p_port: u16,
prune: u32,
}
/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
#[derive(Debug, Clone)]
pub struct InternalBitcoindConfig {
networks: BTreeMap<Network, InternalBitcoindNetworkConfig>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum InternalBitcoindConfigError {
KeyNotFound(String),
CouldNotParseValue(String),
UnexpectedSection(String),
TooManyElements(String),
FileNotFound,
ReadingFile(String),
WritingFile(String),
Unexpected(String),
}
impl std::fmt::Display for InternalBitcoindConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e),
Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e),
Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e),
Self::TooManyElements(section) => {
write!(f, "Section in file contains too many elements: {}", section)
}
Self::FileNotFound => write!(f, "File not found"),
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
}
impl Default for InternalBitcoindConfig {
fn default() -> Self {
Self::new()
}
}
impl InternalBitcoindConfig {
pub fn new() -> Self {
Self {
networks: BTreeMap::new(),
}
}
pub fn from_ini(ini: &ini::Ini) -> Result<Self, InternalBitcoindConfigError> {
let mut networks = BTreeMap::new();
for (maybe_sec, prop) in ini {
if let Some(sec) = maybe_sec {
let network = Network::from_core_arg(sec)
.map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?;
if prop.len() > 3 {
return Err(InternalBitcoindConfigError::TooManyElements(
sec.to_string(),
));
}
let rpc_port = prop
.get("rpcport")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))?
.parse::<u16>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
let p2p_port = prop
.get("port")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
.parse::<u16>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
let prune = prop
.get("prune")
.ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
.parse::<u32>()
.map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
networks.insert(
network,
InternalBitcoindNetworkConfig {
rpc_port,
p2p_port,
prune,
},
);
} else if !prop.is_empty() {
return Err(InternalBitcoindConfigError::UnexpectedSection(
"General section should be empty".to_string(),
));
}
}
Ok(Self { networks })
}
pub fn from_file(path: &PathBuf) -> Result<Self, InternalBitcoindConfigError> {
if !path.exists() {
return Err(InternalBitcoindConfigError::FileNotFound);
}
let conf_ini = ini::Ini::load_from_file(path)
.map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?;
Self::from_ini(&conf_ini)
}
pub fn to_ini(&self) -> ini::Ini {
let mut conf_ini = ini::Ini::new();
for (network, network_conf) in &self.networks {
conf_ini
.with_section(Some(network.to_core_arg()))
.set("rpcport", network_conf.rpc_port.to_string())
.set("port", network_conf.p2p_port.to_string())
.set("prune", network_conf.prune.to_string());
}
conf_ini
}
pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> {
std::fs::create_dir_all(
path.parent()
.ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?,
)
.map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?;
info!("Writing to file {}", path.to_string_lossy());
self.to_ini()
.write_to_file(path)
.map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?;
Ok(())
}
}
/// Path of the `bitcoin.conf` file used by internal bitcoind.
fn internal_bitcoind_config_path(bitcoind_datadir: &PathBuf) -> PathBuf {
let mut config_path = PathBuf::from(bitcoind_datadir);
config_path.push("bitcoin.conf");
config_path
}
/// Path of the cookie file used by internal bitcoind on a given network.
fn internal_bitcoind_cookie_path(bitcoind_datadir: &Path, network: &Network) -> PathBuf {
let mut cookie_path = bitcoind_datadir.to_path_buf();
if let Some(dir) = bitcoind_network_dir(network) {
cookie_path.push(dir);
}
cookie_path.push(".cookie");
cookie_path
}
/// RPC address for internal bitcoind.
fn internal_bitcoind_address(rpc_port: u16) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_port)
}
fn bitcoind_default_datadir() -> Option<PathBuf> {
#[cfg(target_os = "linux")]
let configs_dir = dirs::home_dir();
#[cfg(not(target_os = "linux"))]
let configs_dir = dirs::config_dir();
if let Some(mut path) = configs_dir {
#[cfg(target_os = "linux")]
path.push(".bitcoin");
#[cfg(not(target_os = "linux"))]
path.push("Bitcoin");
return Some(path);
}
None
}
fn bitcoind_network_dir(network: &Network) -> Option<String> {
let dir = match network {
Network::Bitcoin => {
return None;
}
Network::Testnet => "testnet3",
Network::Regtest => "regtest",
Network::Signet => "signet",
_ => panic!("Directory required for this network is unknown."),
};
Some(dir.to_string())
}
fn bitcoind_default_cookie_path(network: &Network) -> Option<String> {
if let Some(mut path) = bitcoind_default_datadir() {
if let Some(dir) = bitcoind_network_dir(network) {
path.push(dir);
}
path.push(".cookie");
return path.to_str().map(|s| s.to_string());
}
None
}
fn bitcoind_default_address(network: &Network) -> String {
match network {
Network::Bitcoin => "127.0.0.1:8332".to_string(),
Network::Testnet => "127.0.0.1:18332".to_string(),
Network::Regtest => "127.0.0.1:18443".to_string(),
Network::Signet => "127.0.0.1:38332".to_string(),
_ => "127.0.0.1:8332".to_string(),
}
}
/// Looks for bitcoind executable path and returns `None` if not found.
fn bitcoind_exe_path() -> Option<PathBuf> {
which::which("bitcoind").ok()
}
/// Get available port that is valid for use by internal bitcoind.
// Modified from https://github.com/RCasatta/bitcoind/blob/f047740d7d0af935ff7360cf77429c5f294cfd59/src/lib.rs#L435
pub fn get_available_port() -> Result<u16, Error> {
// Perform multiple attempts to get a valid port.
for _ in 0..10 {
// Using 0 as port lets the system assign a port available.
let t = TcpListener::bind(("127.0.0.1", 0))
.map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?;
let port = t
.local_addr()
.map(|s| s.port())
.map_err(|e| Error::CannotGetAvailablePort(e.to_string()))?;
if port_is_valid(&port) {
return Ok(port);
}
}
Err(Error::CannotGetAvailablePort(
"Exhausted attempts".to_string(),
))
}
/// Checks if port is valid for use by internal bitcoind.
pub fn port_is_valid(port: &u16) -> bool {
!BITCOIND_DEFAULT_PORTS.contains(port)
}
impl Default for SelectBitcoindTypeStep {
fn default() -> Self {
Self::new()
}
}
impl From<SelectBitcoindTypeStep> for Box<dyn Step> {
fn from(s: SelectBitcoindTypeStep) -> Box<dyn Step> {
Box::new(s)
}
}
impl SelectBitcoindTypeStep {
pub fn new() -> Self {
Self { use_external: true }
}
}
impl Step for SelectBitcoindTypeStep {
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::SelectBitcoindType(msg) = message {
match msg {
message::SelectBitcoindTypeMsg::UseExternal(selected) => {
self.use_external = selected;
}
};
return Command::perform(async {}, |_| Message::Next);
};
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
if !self.use_external {
if ctx.internal_bitcoind_config.is_none() {
ctx.bitcoind_config = None; // Ensures internal bitcoind can be restarted in case user has switched selection.
}
} else {
ctx.internal_bitcoind_config = None;
ctx.internal_bitcoind_exe_config = None;
}
ctx.bitcoind_is_external = self.use_external;
true
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::select_bitcoind_type(progress)
}
}
impl DefineBitcoind {
pub fn new() -> Self {
Self {
cookie_path: form::Value::default(),
address: form::Value::default(),
is_running: None,
}
}
pub fn ping(&self) -> Command<Message> {
let address = self.address.value.to_owned();
let cookie_path = self.cookie_path.value.to_owned();
Command::perform(
async move {
let cookie = std::fs::read_to_string(&cookie_path)
.map_err(|e| Error::Bitcoind(format!("Failed to read cookie file: {}", e)))?;
let client = Client::with_transport(
SimpleHttpTransport::builder()
.url(&address)?
.timeout(std::time::Duration::from_secs(3))
.cookie_auth(cookie)
.build(),
);
client.send_request(client.build_request("echo", &[]))?;
Ok(())
},
|res| Message::DefineBitcoind(message::DefineBitcoind::PingBitcoindResult(res)),
)
}
}
impl Step for DefineBitcoind {
fn load_context(&mut self, ctx: &Context) {
if self.cookie_path.value.is_empty() {
self.cookie_path.value =
bitcoind_default_cookie_path(&ctx.bitcoin_config.network).unwrap_or_default()
}
if self.address.value.is_empty() {
self.address.value = bitcoind_default_address(&ctx.bitcoin_config.network);
}
}
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::DefineBitcoind(msg) = message {
match msg {
message::DefineBitcoind::PingBitcoind => {
self.is_running = None;
return self.ping();
}
message::DefineBitcoind::PingBitcoindResult(res) => self.is_running = Some(res),
message::DefineBitcoind::AddressEdited(address) => {
self.is_running = None;
self.address.value = address;
self.address.valid = true;
}
message::DefineBitcoind::CookiePathEdited(path) => {
self.is_running = None;
self.cookie_path.value = path;
self.address.valid = true;
}
};
};
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
match (
PathBuf::from_str(&self.cookie_path.value),
std::net::SocketAddr::from_str(&self.address.value),
) {
(Err(_), Ok(_)) => {
self.cookie_path.valid = false;
false
}
(Ok(_), Err(_)) => {
self.address.valid = false;
false
}
(Err(_), Err(_)) => {
self.cookie_path.valid = false;
self.address.valid = false;
false
}
(Ok(path), Ok(addr)) => {
ctx.bitcoind_config = Some(BitcoindConfig {
cookie_path: path,
addr,
});
true
}
}
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::define_bitcoin(
progress,
&self.address,
&self.cookie_path,
self.is_running.as_ref(),
)
}
fn load(&self) -> Command<Message> {
self.ping()
}
fn skip(&self, ctx: &Context) -> bool {
!ctx.bitcoind_is_external
}
}
impl Default for DefineBitcoind {
fn default() -> Self {
Self::new()
}
}
impl From<DefineBitcoind> for Box<dyn Step> {
fn from(s: DefineBitcoind) -> Box<dyn Step> {
Box::new(s)
}
}
impl From<InternalBitcoindStep> for Box<dyn Step> {
fn from(s: InternalBitcoindStep) -> Box<dyn Step> {
Box::new(s)
}
}
impl InternalBitcoindStep {
pub fn new(liana_datadir: &PathBuf) -> Self {
Self {
bitcoind_datadir: internal_bitcoind_datadir(liana_datadir),
network: Network::Bitcoin,
started: None,
exe_path: None,
bitcoind_config: None,
exe_config: None,
internal_bitcoind_config: None,
error: None,
}
}
}
impl Step for InternalBitcoindStep {
fn load_context(&mut self, ctx: &Context) {
if self.exe_path.is_none() {
self.exe_path = if let Some(exe_config) = ctx.internal_bitcoind_exe_config.clone() {
Some(exe_config.exe_path)
} else {
bitcoind_exe_path()
};
}
self.network = ctx.bitcoin_config.network;
if let Some(Ok(_)) = self.started {
// This case can arise if a user switches from internal bitcoind to external and back to internal.
if ctx.bitcoind_config.is_none() {
self.started = None; // So that internal bitcoind will be restarted.
}
}
}
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::InternalBitcoind(msg) = message {
match msg {
message::InternalBitcoindMsg::Previous => {
if self.internal_bitcoind_config.is_some() {
if let Some(bitcoind_config) = &self.bitcoind_config {
stop_internal_bitcoind(bitcoind_config);
}
}
return Command::perform(async {}, |_| Message::Previous);
}
message::InternalBitcoindMsg::Reload => {
return self.load();
}
message::InternalBitcoindMsg::DefineConfig => {
let mut conf = match InternalBitcoindConfig::from_file(
&internal_bitcoind_config_path(&self.bitcoind_datadir),
) {
Ok(conf) => conf,
Err(InternalBitcoindConfigError::FileNotFound) => {
InternalBitcoindConfig::new()
}
Err(e) => {
self.error = Some(e.to_string());
return Command::none();
}
};
// Insert entry for network if not present.
if conf.networks.get(&self.network).is_none() {
let network_conf = match (get_available_port(), get_available_port()) {
(Ok(rpc_port), Ok(p2p_port)) => {
// In case ports are the same, user will need to click button again for another attempt.
if rpc_port == p2p_port {
self.error = Some(
"Could not get distinct ports. Please try again."
.to_string(),
);
return Command::none();
}
InternalBitcoindNetworkConfig {
rpc_port,
p2p_port,
prune: PRUNE_DEFAULT,
}
}
(Ok(_), Err(e)) | (Err(e), Ok(_)) => {
self.error = Some(format!("Could not get available port: {}.", e));
return Command::none();
}
(Err(e1), Err(e2)) => {
self.error =
Some(format!("Could not get available ports: {}; {}.", e1, e2));
return Command::none();
}
};
conf.networks.insert(self.network, network_conf);
}
if let Err(e) =
conf.to_file(&internal_bitcoind_config_path(&self.bitcoind_datadir))
{
self.error = Some(e.to_string());
return Command::none();
};
self.error = None;
self.internal_bitcoind_config = Some(conf.clone());
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::Reload)
});
}
message::InternalBitcoindMsg::Start => {
if let Some(path) = &self.exe_path {
let datadir = match self.bitcoind_datadir.canonicalize() {
Ok(datadir) => datadir,
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::CouldNotCanonicalizeDataDir(
e.to_string(),
),
));
return Command::none();
}
};
let exe_config = InternalBitcoindExeConfig {
exe_path: path.to_path_buf(),
data_dir: datadir,
};
if let Err(e) = start_internal_bitcoind(&self.network, exe_config.clone()) {
self.started =
Some(Err(StartInternalBitcoindError::CommandError(e.to_string())));
return Command::none();
}
// Need to wait for cookie file to appear.
let cookie_path =
internal_bitcoind_cookie_path(&self.bitcoind_datadir, &self.network);
if !poll_for_file(&cookie_path, 200, 15) {
self.started =
Some(Err(StartInternalBitcoindError::CookieFileNotFound(
cookie_path.to_string_lossy().into_owned(),
)));
return Command::none();
}
let rpc_port = self
.internal_bitcoind_config
.as_ref()
.expect("Already added")
.clone()
.networks
.get(&self.network)
.expect("Already added")
.rpc_port;
let bitcoind_config = match cookie_path.canonicalize() {
Ok(cookie_path) => BitcoindConfig {
cookie_path,
addr: internal_bitcoind_address(rpc_port),
},
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(
e.to_string(),
),
));
return Command::none();
}
};
match liana::BitcoinD::new(
&bitcoind_config,
"internal_bitcoind_connection_check".to_string(),
) {
Ok(_) => {
self.error = None;
self.bitcoind_config = Some(bitcoind_config);
self.exe_config = Some(exe_config);
self.started = Some(Ok(()));
}
Err(e) => {
self.started = Some(Err(
StartInternalBitcoindError::BitcoinDError(e.to_string()),
));
}
}
}
}
};
};
Command::none()
}
fn load(&self) -> Command<Message> {
if self.internal_bitcoind_config.is_none() {
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::DefineConfig)
});
}
if self.started.is_none() {
return Command::perform(async {}, |_| {
Message::InternalBitcoind(message::InternalBitcoindMsg::Start)
});
}
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
// Any errors have been handled as part of `message::InternalBitcoindMsg::Start`
if let Some(Ok(_)) = self.started {
ctx.bitcoind_config = self.bitcoind_config.clone();
ctx.internal_bitcoind_config = self.internal_bitcoind_config.clone();
ctx.internal_bitcoind_exe_config = self.exe_config.clone();
self.error = None;
return true;
}
false
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::start_internal_bitcoind(
progress,
self.exe_path.as_ref(),
self.started.as_ref(),
self.error.as_ref(),
)
}
fn stop(&self) {
// In case the installer is closed before changes written to context, stop bitcoind.
if let Some(Ok(_)) = self.started {
if let Some(bitcoind_config) = &self.bitcoind_config {
stop_internal_bitcoind(bitcoind_config);
}
}
}
fn skip(&self, ctx: &Context) -> bool {
ctx.bitcoind_is_external
}
}
pub struct Final {
generating: bool,
context: Option<Context>,
@ -828,74 +140,3 @@ impl From<Final> for Box<dyn Step> {
Box::new(s)
}
}
#[cfg(test)]
mod tests {
use crate::installer::step::{InternalBitcoindConfig, InternalBitcoindNetworkConfig};
use ini::Ini;
use liana::miniscript::bitcoin::Network;
// Test the format of the internal bitcoind configuration file.
#[test]
fn internal_bitcoind_config() {
// A valid config
let mut conf_ini = Ini::new();
conf_ini
.with_section(Some("main"))
.set("rpcport", "43345")
.set("port", "42355")
.set("prune", "15246");
conf_ini
.with_section(Some("regtest"))
.set("rpcport", "34067")
.set("port", "45175")
.set("prune", "2043");
let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
let main_conf = InternalBitcoindNetworkConfig {
rpc_port: 43345,
p2p_port: 42355,
prune: 15246,
};
let regtest_conf = InternalBitcoindNetworkConfig {
rpc_port: 34067,
p2p_port: 45175,
prune: 2043,
};
assert_eq!(conf.networks.len(), 2);
assert_eq!(
conf.networks.get(&Network::Bitcoin).expect("Missing main"),
&main_conf
);
assert_eq!(
conf.networks
.get(&Network::Regtest)
.expect("Missing regtest"),
&regtest_conf
);
let mut conf = InternalBitcoindConfig::new();
conf.networks.insert(Network::Bitcoin, main_conf);
conf.networks.insert(Network::Regtest, regtest_conf);
for (sec, prop) in &conf.to_ini() {
if let Some(sec) = sec {
assert_eq!(prop.len(), 3);
let rpc_port = prop.get("rpcport").expect("rpcport");
let p2p_port = prop.get("port").expect("port");
let prune = prop.get("prune").expect("prune");
if sec == "main" {
assert_eq!(rpc_port, "43345");
assert_eq!(p2p_port, "42355");
assert_eq!(prune, "15246");
} else if sec == "regtest" {
assert_eq!(rpc_port, "34067");
assert_eq!(p2p_port, "45175");
assert_eq!(prune, "2043");
} else {
panic!("Unexpected section");
}
} else {
assert!(prop.is_empty())
}
}
}
}