Merge #988: installer: use -rpcauth for managed bitcoind
a997a7bcffac6eecbe472e383b7cb97bd2af738d gui: don't rely on cookie to check successful start (jp1ac4) 5e5c3330ee14159b14e43852d8db1d51a1b6bad8 gui: use rpcauth in installer for internal bitcoind (jp1ac4) 7584b6347bf6525d57d09596a60b4bdd2bccfb7f gui: start internal bitcoind with given config (jp1ac4) b5980fbc4f6be006b083fbc8c7e4142e50598fb8 gui: add sections check to internal bitcoind config test (jp1ac4) c3aad0f40c94ae86d1b14ff1b05178c0c043f732 gui: add optional rpcauth to internal bitcoind config (jp1ac4) e172ecd2f1e910a5ba16ea4debb0091b70c2cb17 gui: move internal bitcoind config to bitcoind module (jp1ac4) Pull request description: This is to resolve #929. The installer will generate an `rpcauth=` string for use in the managed bitcoind's bitcoin.conf file and will use the corresponding user and password in the Liana daemon config file. ~~The existence of the cookie file is still checked by `Bitcoind::start()` to determine if the process started successfully. An alternative approach could be considered in a follow-up PR (or as part of this one).~~ For https://github.com/wizardsardine/liana/issues/924, we might still need to use the cookie path to stop an already-running process, as we would probably no longer have the password used to generate the previous `rpcauth=` string. Once https://github.com/wizardsardine/liana/pull/987 is merged, I'll update the Cargo files in this PR. In the first commit, I move some code from the installer to the bitcoind module. In the end, this wasn't strictly required for the other changes I made, but I kept it as I thought it made sense anyway. ACKs for top commit: edouardparis: ACK a997a7bcffac6eecbe472e383b7cb97bd2af738d Tree-SHA512: ec475bb3c82db4e30c42612e93c63e9b0387311a26d5433421f275e24ac5b0923eba9bd1daff612e60a6cf0bdcc10de0812ff69972237e6785c0835b5a31450f
This commit is contained in:
commit
87712ab043
3
gui/Cargo.lock
generated
3
gui/Cargo.lock
generated
@ -2668,7 +2668,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "liana"
|
||||
version = "4.0.0"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#5a56fdb108351d0a6877a11a1dec7a27a3b0928f"
|
||||
source = "git+https://github.com/wizardsardine/liana?branch=master#16afa3e9925cd016db03dbe954403bfa348b89e7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bdk_coin_select",
|
||||
@ -2692,6 +2692,7 @@ version = "4.0.0"
|
||||
dependencies = [
|
||||
"async-hwi",
|
||||
"backtrace",
|
||||
"base64 0.21.6",
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"chrono",
|
||||
"dirs 3.0.2",
|
||||
|
||||
@ -42,6 +42,7 @@ toml = "0.5"
|
||||
chrono = "0.4"
|
||||
|
||||
# Used for managing internal bitcoind
|
||||
base64 = "0.21"
|
||||
bitcoin_hashes = "0.12"
|
||||
reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] }
|
||||
rust-ini = "0.19.0"
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
use base64::Engine;
|
||||
use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
|
||||
use liana::{
|
||||
config::{BitcoindConfig, BitcoindRpcAuth},
|
||||
config::BitcoindConfig,
|
||||
miniscript::bitcoin::{self, Network},
|
||||
random::{random_bytes, RandomnessError},
|
||||
};
|
||||
use liana_ui::component::form;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@ -120,16 +124,243 @@ pub fn bitcoind_network_dir(network: &Network) -> Option<String> {
|
||||
Some(dir.to_string())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum RpcAuthParseError {
|
||||
MissingColon,
|
||||
MissingDollarSign,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RpcAuthParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingColon => write!(
|
||||
f,
|
||||
"RPC auth string should contain colon between user and salt."
|
||||
),
|
||||
Self::MissingDollarSign => write!(
|
||||
f,
|
||||
"RPC auth string should contain dollar sign between salt and password HMAC."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents RPC auth credentials as stored in bitcoin.conf.
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct RpcAuth {
|
||||
pub user: String,
|
||||
salt: String,
|
||||
password_hmac: String,
|
||||
}
|
||||
|
||||
impl RpcAuth {
|
||||
/// Returns a new `RpcAuth` object for the given `user` with a random salt and password.
|
||||
/// This random password is also returned.
|
||||
pub fn new(user: &str) -> Result<(Self, String), RandomnessError> {
|
||||
// RPC auth generation follows approach in
|
||||
// https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py
|
||||
let password =
|
||||
random_bytes().map(|bytes| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(bytes))?;
|
||||
// As per the Python script, only use 16 bytes for the salt.
|
||||
let salt = random_bytes().map(|bytes| hex::encode(&bytes[..16]))?;
|
||||
let mut engine = HmacEngine::<sha256::Hash>::new(salt.as_bytes());
|
||||
engine.input(password.as_bytes());
|
||||
let password_hmac = Hmac::<sha256::Hash>::from_engine(engine);
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
user: user.to_string(),
|
||||
salt,
|
||||
password_hmac: hex::encode(&password_hmac[..]),
|
||||
},
|
||||
password,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RpcAuth {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}:{}${}", self.user, self.salt, self.password_hmac)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for RpcAuth {
|
||||
type Err = RpcAuthParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (user, salt_pw) = s.split_once(':').ok_or(RpcAuthParseError::MissingColon)?;
|
||||
let (salt, pw) = salt_pw
|
||||
.split_once('$')
|
||||
.ok_or(RpcAuthParseError::MissingDollarSign)?;
|
||||
Ok(Self {
|
||||
user: user.to_string(),
|
||||
salt: salt.to_string(),
|
||||
password_hmac: pw.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents section for a single network in `bitcoin.conf` file.
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct InternalBitcoindNetworkConfig {
|
||||
pub rpc_port: u16,
|
||||
pub p2p_port: u16,
|
||||
pub prune: u32,
|
||||
pub rpc_auth: Option<RpcAuth>,
|
||||
}
|
||||
|
||||
/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InternalBitcoindConfig {
|
||||
pub 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() > 4 {
|
||||
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()))?;
|
||||
let rpc_auth = prop
|
||||
.get("rpcauth")
|
||||
.map(|v| {
|
||||
v.parse::<RpcAuth>().map_err(|e| {
|
||||
InternalBitcoindConfigError::CouldNotParseValue(e.to_string())
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
networks.insert(
|
||||
network,
|
||||
InternalBitcoindNetworkConfig {
|
||||
rpc_port,
|
||||
p2p_port,
|
||||
prune,
|
||||
rpc_auth,
|
||||
},
|
||||
);
|
||||
} 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());
|
||||
if let Some(rpc_auth) = network_conf.rpc_auth.as_ref() {
|
||||
conf_ini
|
||||
.with_section(Some(network.to_core_arg()))
|
||||
.set("rpcauth", rpc_auth.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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible errors when starting bitcoind.
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum StartInternalBitcoindError {
|
||||
CommandError(String),
|
||||
CouldNotCanonicalizeExePath(String),
|
||||
CouldNotCanonicalizeDataDir(String),
|
||||
CouldNotCanonicalizeCookiePath(String),
|
||||
CookieFileNotFound(String),
|
||||
BitcoinDError(String),
|
||||
ExecutableNotFound,
|
||||
ProcessExited(std::process::ExitStatus),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StartInternalBitcoindError {
|
||||
@ -138,24 +369,14 @@ impl std::fmt::Display for StartInternalBitcoindError {
|
||||
Self::CommandError(e) => {
|
||||
write!(f, "Command to start bitcoind returned an error: {}", e)
|
||||
}
|
||||
Self::CouldNotCanonicalizeExePath(e) => {
|
||||
write!(f, "Failed to canonicalize executable path: {}", e)
|
||||
}
|
||||
Self::CouldNotCanonicalizeDataDir(e) => {
|
||||
write!(f, "Failed to canonicalize datadir: {}", e)
|
||||
}
|
||||
Self::CouldNotCanonicalizeCookiePath(e) => {
|
||||
write!(f, "Failed to canonicalize cookie path: {}", e)
|
||||
}
|
||||
Self::CookieFileNotFound(path) => {
|
||||
write!(
|
||||
f,
|
||||
"Cookie file was not found at the expected path: {}",
|
||||
path
|
||||
)
|
||||
}
|
||||
Self::BitcoinDError(e) => write!(f, "bitcoind connection check failed: {}", e),
|
||||
Self::ExecutableNotFound => write!(f, "bitcoind executable not found."),
|
||||
Self::ProcessExited(status) => {
|
||||
write!(f, "bitcoind process exited with status '{}'.", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,7 +390,7 @@ impl Bitcoind {
|
||||
/// Start internal bitcoind for the given network.
|
||||
pub fn start(
|
||||
network: &bitcoin::Network,
|
||||
mut config: BitcoindConfig,
|
||||
config: BitcoindConfig,
|
||||
liana_datadir: &PathBuf,
|
||||
) -> Result<Self, StartInternalBitcoindError> {
|
||||
let bitcoind_datadir = internal_bitcoind_datadir(liana_datadir);
|
||||
@ -222,39 +443,43 @@ impl Bitcoind {
|
||||
.map_err(|e| StartInternalBitcoindError::CommandError(e.to_string()))?;
|
||||
|
||||
// We've started bitcoind in the background, however it may fail to start for whatever
|
||||
// reason. And we need its JSONRPC interface to be available to continue. Thus wait for it
|
||||
// to have created the cookie file, regularly checking it did not fail to start.
|
||||
let cookie_path = internal_bitcoind_cookie_path(&bitcoind_datadir, network);
|
||||
// reason. And we need its JSONRPC interface to be available to continue. Thus wait for
|
||||
// the interface to be created successfully, regularly checking it did not fail to start.
|
||||
loop {
|
||||
match process.try_wait() {
|
||||
Ok(None) => {}
|
||||
Err(e) => log::error!("Error while trying to wait for bitcoind: {}", e),
|
||||
Ok(Some(status)) => {
|
||||
log::error!("Bitcoind exited with status '{}'", status);
|
||||
return Err(StartInternalBitcoindError::CookieFileNotFound(
|
||||
cookie_path.to_string_lossy().into_owned(),
|
||||
));
|
||||
return Err(StartInternalBitcoindError::ProcessExited(status));
|
||||
}
|
||||
}
|
||||
if cookie_path.exists() {
|
||||
match liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) {
|
||||
Ok(_) => {
|
||||
log::info!("Bitcoind seems to have successfully started.");
|
||||
break;
|
||||
return Ok(Self {
|
||||
config,
|
||||
_process: Arc::new(process),
|
||||
});
|
||||
}
|
||||
Err(liana::BitcoindError::CookieFile(_)) => {
|
||||
// This is only raised if we're using cookie authentication.
|
||||
// Assume cookie file has not been created yet and try again.
|
||||
}
|
||||
Err(e) => {
|
||||
if !e.is_transient() {
|
||||
// Non-transient error could happen, e.g., if RPC auth credentials are wrong.
|
||||
// Kill process now in case it's not possible to do via RPC command later.
|
||||
if let Err(e) = process.kill() {
|
||||
log::error!("Error trying to kill bitcoind process: '{}'", e);
|
||||
}
|
||||
return Err(StartInternalBitcoindError::BitcoinDError(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Waiting for bitcoind to start.");
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
config.rpc_auth = BitcoindRpcAuth::CookieFile(cookie_path.canonicalize().map_err(|e| {
|
||||
StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(e.to_string())
|
||||
})?);
|
||||
|
||||
liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string())
|
||||
.map_err(|e| StartInternalBitcoindError::BitcoinDError(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
_process: Arc::new(process),
|
||||
})
|
||||
}
|
||||
|
||||
/// Stop (internal) bitcoind.
|
||||
@ -318,3 +543,88 @@ impl fmt::Display for ConfigField {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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")
|
||||
.set("rpcauth", "my_user:my_salt$my_pw_hmac");
|
||||
let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
|
||||
let main_conf = InternalBitcoindNetworkConfig {
|
||||
rpc_port: 43345,
|
||||
p2p_port: 42355,
|
||||
prune: 15246,
|
||||
rpc_auth: None,
|
||||
};
|
||||
let regtest_conf = InternalBitcoindNetworkConfig {
|
||||
rpc_port: 34067,
|
||||
p2p_port: 45175,
|
||||
prune: 2043,
|
||||
rpc_auth: Some(RpcAuth {
|
||||
user: "my_user".to_string(),
|
||||
salt: "my_salt".to_string(),
|
||||
password_hmac: "my_pw_hmac".to_string(),
|
||||
}),
|
||||
};
|
||||
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"),
|
||||
®test_conf
|
||||
);
|
||||
|
||||
let mut conf = InternalBitcoindConfig::new();
|
||||
conf.networks.insert(Network::Bitcoin, main_conf);
|
||||
conf.networks.insert(Network::Regtest, regtest_conf);
|
||||
conf_ini = conf.to_ini();
|
||||
assert_eq!(conf_ini.len(), 3); // 2 network sections plus the empty general section
|
||||
assert!(conf_ini.general_section().is_empty());
|
||||
for (sec, prop) in &conf_ini {
|
||||
if let Some(sec) = sec {
|
||||
let rpc_port = prop.get("rpcport").expect("rpcport");
|
||||
let p2p_port = prop.get("port").expect("port");
|
||||
let prune = prop.get("prune").expect("prune");
|
||||
let rpc_auth = prop.get("rpcauth");
|
||||
if sec == "main" {
|
||||
assert_eq!(prop.len(), 3);
|
||||
assert_eq!(rpc_port, "43345");
|
||||
assert_eq!(p2p_port, "42355");
|
||||
assert_eq!(prune, "15246");
|
||||
assert!(rpc_auth.is_none());
|
||||
} else if sec == "regtest" {
|
||||
assert_eq!(prop.len(), 4);
|
||||
assert_eq!(rpc_port, "34067");
|
||||
assert_eq!(p2p_port, "45175");
|
||||
assert_eq!(prune, "2043");
|
||||
assert_eq!(rpc_auth, Some("my_user:my_salt$my_pw_hmac"));
|
||||
} else {
|
||||
panic!("Unexpected section");
|
||||
}
|
||||
} else {
|
||||
assert!(prop.is_empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,9 @@ impl Daemon for EmbeddedDaemon {
|
||||
}
|
||||
|
||||
fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
|
||||
Ok(self.control()?.list_spend())
|
||||
self.control()?
|
||||
.list_spend(None)
|
||||
.map_err(|e| DaemonError::Unexpected(e.to_string()))
|
||||
}
|
||||
|
||||
fn list_confirmed_txs(
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::{
|
||||
settings::{KeySetting, Settings, WalletSetting},
|
||||
wallet::wallet_name,
|
||||
},
|
||||
bitcoind::Bitcoind,
|
||||
bitcoind::{Bitcoind, InternalBitcoindConfig},
|
||||
hw::HardwareWalletConfig,
|
||||
signer::Signer,
|
||||
};
|
||||
@ -19,8 +19,6 @@ use liana::{
|
||||
miniscript::bitcoin,
|
||||
};
|
||||
|
||||
use super::step::InternalBitcoindConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub bitcoin_config: BitcoinConfig,
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::{self, Cursor};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
|
||||
@ -24,7 +23,9 @@ use liana_ui::{component::form, widget::*};
|
||||
use crate::{
|
||||
bitcoind::{
|
||||
self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory,
|
||||
Bitcoind, ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError, VERSION,
|
||||
Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError,
|
||||
InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues,
|
||||
StartInternalBitcoindError, VERSION,
|
||||
},
|
||||
download,
|
||||
hw::HardwareWallets,
|
||||
@ -108,143 +109,6 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InstallState {
|
||||
InProgress,
|
||||
@ -734,9 +598,12 @@ impl Step for InternalBitcoindStep {
|
||||
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()) {
|
||||
let (rpc_port, p2p_port) = if let Some(network_conf) =
|
||||
conf.networks.get(&self.network)
|
||||
{
|
||||
(network_conf.rpc_port, network_conf.p2p_port)
|
||||
} else {
|
||||
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 {
|
||||
@ -746,11 +613,7 @@ impl Step for InternalBitcoindStep {
|
||||
);
|
||||
return Command::none();
|
||||
}
|
||||
InternalBitcoindNetworkConfig {
|
||||
rpc_port,
|
||||
p2p_port,
|
||||
prune: PRUNE_DEFAULT,
|
||||
}
|
||||
(rpc_port, p2p_port)
|
||||
}
|
||||
(Ok(_), Err(e)) | (Err(e), Ok(_)) => {
|
||||
self.error = Some(format!("Could not get available port: {}.", e));
|
||||
@ -761,9 +624,27 @@ impl Step for InternalBitcoindStep {
|
||||
Some(format!("Could not get available ports: {}; {}.", e1, e2));
|
||||
return Command::none();
|
||||
}
|
||||
}
|
||||
};
|
||||
let (rpc_auth, rpc_password) = match RpcAuth::new("liana") {
|
||||
Ok((rpc_auth, password)) => (rpc_auth, password),
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string());
|
||||
return Command::none();
|
||||
}
|
||||
};
|
||||
let bitcoind_config = BitcoindConfig {
|
||||
rpc_auth: BitcoindRpcAuth::UserPass(rpc_auth.user.clone(), rpc_password),
|
||||
addr: internal_bitcoind_address(rpc_port),
|
||||
};
|
||||
let network_conf = InternalBitcoindNetworkConfig {
|
||||
rpc_port,
|
||||
p2p_port,
|
||||
prune: PRUNE_DEFAULT,
|
||||
// Overwrite any previous entry for this network as we would no longer know the RPC password.
|
||||
rpc_auth: Some(rpc_auth),
|
||||
};
|
||||
conf.networks.insert(self.network, network_conf);
|
||||
}
|
||||
if let Err(e) = conf.to_file(&bitcoind::internal_bitcoind_config_path(
|
||||
&self.bitcoind_datadir,
|
||||
)) {
|
||||
@ -771,7 +652,8 @@ impl Step for InternalBitcoindStep {
|
||||
return Command::none();
|
||||
};
|
||||
self.error = None;
|
||||
self.internal_bitcoind_config = Some(conf.clone());
|
||||
self.internal_bitcoind_config = Some(conf);
|
||||
self.bitcoind_config = Some(bitcoind_config);
|
||||
return Command::perform(async {}, |_| {
|
||||
Message::InternalBitcoind(message::InternalBitcoindMsg::Reload)
|
||||
});
|
||||
@ -833,31 +715,13 @@ impl Step for InternalBitcoindStep {
|
||||
StartInternalBitcoindError::CouldNotCanonicalizeDataDir(e.to_string()),
|
||||
));
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
let cookie_path = bitcoind::internal_bitcoind_cookie_path(
|
||||
&self.bitcoind_datadir,
|
||||
&self.network,
|
||||
);
|
||||
|
||||
let rpc_port = self
|
||||
.internal_bitcoind_config
|
||||
};
|
||||
let bitcoind_config = self
|
||||
.bitcoind_config
|
||||
.as_ref()
|
||||
.expect("Already added")
|
||||
.clone()
|
||||
.networks
|
||||
.get(&self.network)
|
||||
.expect("Already added")
|
||||
.rpc_port;
|
||||
|
||||
match Bitcoind::start(
|
||||
&self.network,
|
||||
BitcoindConfig {
|
||||
rpc_auth: BitcoindRpcAuth::CookieFile(cookie_path),
|
||||
addr: internal_bitcoind_address(rpc_port),
|
||||
},
|
||||
&self.liana_datadir,
|
||||
) {
|
||||
.expect("already added")
|
||||
.clone();
|
||||
match Bitcoind::start(&self.network, bitcoind_config, &self.liana_datadir) {
|
||||
Err(e) => {
|
||||
self.started =
|
||||
Some(Err(StartInternalBitcoindError::CommandError(e.to_string())));
|
||||
@ -865,7 +729,6 @@ impl Step for InternalBitcoindStep {
|
||||
}
|
||||
Ok(bitcoind) => {
|
||||
self.error = None;
|
||||
self.bitcoind_config = Some(bitcoind.config.clone());
|
||||
self.started = Some(Ok(()));
|
||||
self.internal_bitcoind = Some(bitcoind);
|
||||
}
|
||||
@ -941,75 +804,7 @@ impl Step for InternalBitcoindStep {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::installer::step::bitcoind::{
|
||||
verify_hash, 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"),
|
||||
®test_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())
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::verify_hash;
|
||||
|
||||
#[test]
|
||||
fn hash() {
|
||||
|
||||
@ -3,8 +3,7 @@ mod descriptor;
|
||||
mod mnemonic;
|
||||
|
||||
pub use bitcoind::{
|
||||
DefineBitcoind, DownloadState, InstallState, InternalBitcoindConfig, InternalBitcoindStep,
|
||||
SelectBitcoindTypeStep,
|
||||
DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep,
|
||||
};
|
||||
|
||||
pub use descriptor::{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user