From 74820d97e9257e44c5c9d3bc64926e30066b285a Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 19 Nov 2024 15:27:48 +0100 Subject: [PATCH 1/2] Split into lianad and liana crate --- .cirrus.yml | 2 +- Cargo.lock | 27 +- Cargo.toml | 3 +- liana-gui/Cargo.toml | 3 +- liana-gui/src/app/error.rs | 3 +- liana-gui/src/app/message.rs | 12 +- liana-gui/src/app/mod.rs | 3 +- liana-gui/src/app/state/coins.rs | 2 +- liana-gui/src/app/state/mod.rs | 6 +- liana-gui/src/app/state/psbt.rs | 2 +- liana-gui/src/app/state/recovery.rs | 10 +- liana-gui/src/app/state/settings/bitcoind.rs | 12 +- liana-gui/src/app/state/spend/mod.rs | 6 +- liana-gui/src/app/state/spend/step.rs | 2 +- liana-gui/src/app/state/transactions.rs | 2 +- liana-gui/src/app/view/settings.rs | 6 +- liana-gui/src/daemon/client/mod.rs | 7 +- liana-gui/src/daemon/embedded.rs | 4 +- liana-gui/src/daemon/mod.rs | 8 +- liana-gui/src/daemon/model.rs | 9 +- liana-gui/src/installer/context.rs | 7 +- liana-gui/src/installer/mod.rs | 12 +- liana-gui/src/installer/step/node/bitcoind.rs | 8 +- liana-gui/src/installer/step/node/electrum.rs | 6 +- liana-gui/src/launcher.rs | 5 +- liana-gui/src/lianalite/client/backend/mod.rs | 6 +- liana-gui/src/lib.rs | 2 +- liana-gui/src/loader.rs | 14 +- liana-gui/src/main.rs | 3 +- liana-gui/src/node/bitcoind.rs | 8 +- liana-gui/src/node/mod.rs | 2 +- liana/Cargo.toml | 37 +- liana/src/lib.rs | 867 ----------------- liana/src/signer.rs | 19 +- lianad/Cargo.toml | 54 ++ {liana => lianad}/src/bin/cli.rs | 2 +- {liana => lianad}/src/bin/daemon.rs | 2 +- {liana => lianad}/src/bitcoin/d/mod.rs | 2 +- {liana => lianad}/src/bitcoin/d/utils.rs | 0 .../src/bitcoin/electrum/client.rs | 0 {liana => lianad}/src/bitcoin/electrum/mod.rs | 0 .../src/bitcoin/electrum/utils.rs | 0 .../src/bitcoin/electrum/wallet.rs | 6 +- {liana => lianad}/src/bitcoin/mod.rs | 6 +- .../src/bitcoin/poller/looper.rs | 2 +- {liana => lianad}/src/bitcoin/poller/mod.rs | 3 +- {liana => lianad}/src/commands/mod.rs | 14 +- {liana => lianad}/src/commands/utils.rs | 0 {liana => lianad}/src/config.rs | 2 +- {liana => lianad}/src/database/mod.rs | 0 {liana => lianad}/src/database/sqlite/mod.rs | 2 +- .../src/database/sqlite/schema.rs | 2 +- .../src/database/sqlite/utils.rs | 0 {liana => lianad}/src/jsonrpc/api.rs | 0 {liana => lianad}/src/jsonrpc/mod.rs | 0 {liana => lianad}/src/jsonrpc/rpc.rs | 0 {liana => lianad}/src/jsonrpc/server/mod.rs | 0 {liana => lianad}/src/jsonrpc/server/unix.rs | 0 lianad/src/lib.rs | 870 ++++++++++++++++++ {liana => lianad}/src/testutils.rs | 3 +- 60 files changed, 1068 insertions(+), 1027 deletions(-) create mode 100644 lianad/Cargo.toml rename {liana => lianad}/src/bin/cli.rs (99%) rename {liana => lianad}/src/bin/daemon.rs (98%) rename {liana => lianad}/src/bitcoin/d/mod.rs (99%) rename {liana => lianad}/src/bitcoin/d/utils.rs (100%) rename {liana => lianad}/src/bitcoin/electrum/client.rs (100%) rename {liana => lianad}/src/bitcoin/electrum/mod.rs (100%) rename {liana => lianad}/src/bitcoin/electrum/utils.rs (100%) rename {liana => lianad}/src/bitcoin/electrum/wallet.rs (99%) rename {liana => lianad}/src/bitcoin/mod.rs (99%) rename {liana => lianad}/src/bitcoin/poller/looper.rs (99%) rename {liana => lianad}/src/bitcoin/poller/mod.rs (99%) rename {liana => lianad}/src/commands/mod.rs (99%) rename {liana => lianad}/src/commands/utils.rs (100%) rename {liana => lianad}/src/config.rs (99%) rename {liana => lianad}/src/database/mod.rs (100%) rename {liana => lianad}/src/database/sqlite/mod.rs (99%) rename {liana => lianad}/src/database/sqlite/schema.rs (99%) rename {liana => lianad}/src/database/sqlite/utils.rs (100%) rename {liana => lianad}/src/jsonrpc/api.rs (100%) rename {liana => lianad}/src/jsonrpc/mod.rs (100%) rename {liana => lianad}/src/jsonrpc/rpc.rs (100%) rename {liana => lianad}/src/jsonrpc/server/mod.rs (100%) rename {liana => lianad}/src/jsonrpc/server/unix.rs (100%) create mode 100644 lianad/src/lib.rs rename {liana => lianad}/src/testutils.rs (99%) diff --git a/.cirrus.yml b/.cirrus.yml index 6129305ba..992b0a288 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -75,7 +75,7 @@ task: fingerprint_script: - rustc --version - cat tests/tools/taproot_signer/Cargo.lock - lianad_build_script: cd liana && cargo build --release && cd ../tests/tools/taproot_signer && cargo build --release + lianad_build_script: cd lianad && cargo build --release && cd ../tests/tools/taproot_signer && cargo build --release deps_script: apt update && apt install -y python3 python3-pip diff --git a/Cargo.lock b/Cargo.lock index 045c68b66..09fe41afd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2804,21 +2804,13 @@ dependencies = [ name = "liana" version = "8.0.0" dependencies = [ - "backtrace", "bdk_coin_select", - "bdk_electrum", "bip39", - "dirs 5.0.1", - "fern", "getrandom", - "jsonrpc 0.17.0", "log", "miniscript", "rdrand", - "rusqlite", "serde", - "serde_json", - "toml", ] [[package]] @@ -2850,6 +2842,7 @@ dependencies = [ "jsonrpc 0.12.1", "liana", "liana-ui", + "lianad", "log", "reqwest", "rust-ini", @@ -2872,6 +2865,24 @@ dependencies = [ "iced", ] +[[package]] +name = "lianad" +version = "8.0.0" +dependencies = [ + "backtrace", + "bdk_electrum", + "dirs 5.0.1", + "fern", + "jsonrpc 0.17.0", + "liana", + "log", + "miniscript", + "rusqlite", + "serde", + "serde_json", + "toml", +] + [[package]] name = "libc" version = "0.2.162" diff --git a/Cargo.toml b/Cargo.toml index ca948f904..a1e82188c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,11 @@ resolver = "2" members = [ "fuzz", "liana", + "lianad", "liana-gui", "liana-ui", ] -default-members = ["liana", "liana-gui", "liana-ui"] +default-members = ["liana", "lianad", "liana-gui", "liana-ui"] [patch.crates-io] iced_style = { git = "https://github.com/edouardparis/iced", branch = "patch-0.12.3"} diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 94ba004e3..98e97ca9b 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -16,7 +16,8 @@ path = "src/main.rs" [dependencies] async-trait = "0.1" async-hwi = { version = "0.0.24" } -liana = { path = "../liana", default-features = false, features = ["nonblocking_shutdown"] } +liana = { path = "../liana" } +lianad = { path = "../lianad", default-features = false, features = ["nonblocking_shutdown"] } liana-ui = { path = "../liana-ui" } backtrace = "0.3" hex = "0.4.3" diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index 2ca312d77..4f2792427 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -1,7 +1,8 @@ use std::convert::From; use std::io::ErrorKind; -use liana::{config::ConfigError, descriptors::LianaDescError, spend::SpendCreationError}; +use liana::{descriptors::LianaDescError, spend::SpendCreationError}; +use lianad::config::ConfigError; use crate::{ app::{settings::SettingsError, wallet::WalletError}, diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index ea5e0ef6b..d0ae6af56 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -1,14 +1,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use liana::{ - config::Config as DaemonConfig, - miniscript::bitcoin::{ - bip32::{ChildNumber, Fingerprint}, - psbt::Psbt, - Address, Txid, - }, +use liana::miniscript::bitcoin::{ + bip32::{ChildNumber, Fingerprint}, + psbt::Psbt, + Address, Txid, }; +use lianad::config::Config as DaemonConfig; use crate::{ app::{cache::Cache, error::Error, view, wallet::Wallet}, diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index ac9f69cc6..6d6536fde 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -19,11 +19,12 @@ use iced::{clipboard, time, Command, Subscription}; use tokio::runtime::Handle; use tracing::{error, info, warn}; -pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin}; +pub use liana::miniscript::bitcoin; use liana_ui::{ component::network_banner, widget::{Column, Element}, }; +pub use lianad::{commands::CoinStatus, config::Config as DaemonConfig}; pub use config::Config; pub use message::Message; diff --git a/liana-gui/src/app/state/coins.rs b/liana-gui/src/app/state/coins.rs index 173c866cc..c7efb89ea 100644 --- a/liana-gui/src/app/state/coins.rs +++ b/liana-gui/src/app/state/coins.rs @@ -4,8 +4,8 @@ use std::{cmp::Ordering, collections::HashSet}; use iced::Command; -use liana::commands::CoinStatus; use liana_ui::widget::Element; +use lianad::commands::CoinStatus; use crate::{ app::{ diff --git a/liana-gui/src/app/state/mod.rs b/liana-gui/src/app/state/mod.rs index 8162faade..cda029120 100644 --- a/liana-gui/src/app/state/mod.rs +++ b/liana-gui/src/app/state/mod.rs @@ -13,11 +13,9 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use iced::{Command, Subscription}; -use liana::{ - commands::CoinStatus, - miniscript::bitcoin::{Amount, OutPoint}, -}; +use liana::miniscript::bitcoin::{Amount, OutPoint}; use liana_ui::widget::*; +use lianad::commands::CoinStatus; use super::{ cache::Cache, diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index dad7ca034..b736219b9 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -7,10 +7,10 @@ use iced::Subscription; use iced::Command; use liana::{ - commands::CoinStatus, descriptors::LianaPolicy, miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network, Txid}, }; +use lianad::commands::CoinStatus; use liana_ui::component::toast; use liana_ui::{ diff --git a/liana-gui/src/app/state/recovery.rs b/liana-gui/src/app/state/recovery.rs index 09298aaff..a9a9f4349 100644 --- a/liana-gui/src/app/state/recovery.rs +++ b/liana-gui/src/app/state/recovery.rs @@ -4,14 +4,12 @@ use std::sync::Arc; use iced::Command; -use liana::{ - commands::CoinStatus, - miniscript::bitcoin::{ - bip32::{DerivationPath, Fingerprint}, - secp256k1, - }, +use liana::miniscript::bitcoin::{ + bip32::{DerivationPath, Fingerprint}, + secp256k1, }; use liana_ui::{component::form, widget::Element}; +use lianad::commands::CoinStatus; use crate::{ app::{ diff --git a/liana-gui/src/app/state/settings/bitcoind.rs b/liana-gui/src/app/state/settings/bitcoind.rs index 52c59e251..9be01bb50 100644 --- a/liana-gui/src/app/state/settings/bitcoind.rs +++ b/liana-gui/src/app/state/settings/bitcoind.rs @@ -8,11 +8,9 @@ use chrono::{NaiveDate, Utc}; use iced::Command; use tracing::info; -use liana::{ - config::{ - BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig, - }, - miniscript::bitcoin::Network, +use liana::miniscript::bitcoin::Network; +use lianad::config::{ + BitcoinBackend, BitcoinConfig, BitcoindConfig, BitcoindRpcAuth, Config, ElectrumConfig, }; use liana_ui::{component::form, widget::Element}; @@ -353,7 +351,7 @@ impl BitcoindSettings { if let (true, Some(rpc_auth)) = (self.addr.valid, rpc_auth) { let mut daemon_config = daemon.config().cloned().unwrap(); daemon_config.bitcoin_backend = - Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig { + Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig { rpc_auth, addr: new_addr.unwrap(), })); @@ -461,7 +459,7 @@ impl ElectrumSettings { if self.addr.valid { let mut daemon_config = daemon.config().cloned().unwrap(); daemon_config.bitcoin_backend = - Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig { + Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { addr: self.addr.value.clone(), })); self.processing = true; diff --git a/liana-gui/src/app/state/spend/mod.rs b/liana-gui/src/app/state/spend/mod.rs index ed5c6010a..61a581f0a 100644 --- a/liana-gui/src/app/state/spend/mod.rs +++ b/liana-gui/src/app/state/spend/mod.rs @@ -5,11 +5,9 @@ use std::sync::Arc; use iced::Command; -use liana::{ - commands::CoinStatus, - miniscript::bitcoin::{Network, OutPoint}, -}; +use liana::miniscript::bitcoin::{Network, OutPoint}; use liana_ui::widget::Element; +use lianad::commands::CoinStatus; use super::{redirect, State}; use crate::{ diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 69f244bba..79a2937eb 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -8,13 +8,13 @@ use std::{ use iced::{Command, Subscription}; use liana::{ - commands::ListCoinsEntry, descriptors::LianaDescriptor, miniscript::bitcoin::{ address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint, }, spend::{SpendCreationError, MAX_FEERATE}, }; +use lianad::commands::ListCoinsEntry; use liana_ui::{component::form, widget::Element}; diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index c7285109f..8a7c334af 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -7,7 +7,6 @@ use std::{ use iced::Command; use liana::{ - commands::CoinStatus, miniscript::bitcoin::{OutPoint, Txid}, spend::{SpendCreationError, MAX_FEERATE}, }; @@ -15,6 +14,7 @@ use liana_ui::{ component::{form, modal::Modal}, widget::*, }; +use lianad::commands::CoinStatus; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; diff --git a/liana-gui/src/app/view/settings.rs b/liana-gui/src/app/view/settings.rs index a597a2dbd..72d986e05 100644 --- a/liana-gui/src/app/view/settings.rs +++ b/liana-gui/src/app/view/settings.rs @@ -8,10 +8,10 @@ use iced::{ }; use liana::{ - config::BitcoindRpcAuth, descriptors::{LianaDescriptor, LianaPolicy}, miniscript::bitcoin::{bip32::Fingerprint, Network}, }; +use lianad::config::BitcoindRpcAuth; use super::{dashboard, message::*}; @@ -452,7 +452,7 @@ pub fn bitcoind_edit<'a>( pub fn bitcoind<'a>( is_configured_node_type: bool, network: Network, - config: &liana::config::BitcoindConfig, + config: &lianad::config::BitcoindConfig, blockheight: i32, is_running: Option, can_edit: bool, @@ -638,7 +638,7 @@ pub fn electrum_edit<'a>( pub fn electrum<'a>( is_configured_node_type: bool, network: Network, - config: &liana::config::ElectrumConfig, + config: &lianad::config::ElectrumConfig, blockheight: i32, is_running: Option, can_edit: bool, diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 23aba5530..ec6abe03c 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -4,7 +4,6 @@ use std::iter::FromIterator; use std::path::Path; use async_trait::async_trait; -use liana::commands::{CoinStatus, CreateRecoveryResult}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -13,10 +12,10 @@ use tracing::{error, info}; pub mod error; pub mod jsonrpc; -use liana::{ - commands::LabelItem, +use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}; +use lianad::{ + commands::{CoinStatus, CreateRecoveryResult, LabelItem}, config::Config, - miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, }; use super::{model::*, Daemon, DaemonBackend, DaemonError}; diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 5bb399c8f..06318b033 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -4,10 +4,10 @@ use tokio::sync::Mutex; use super::{model::*, node, Daemon, DaemonBackend, DaemonError}; use async_trait::async_trait; -use liana::{ +use liana::miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}; +use lianad::{ commands::{CoinStatus, LabelItem}, config::Config, - miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, DaemonControl, DaemonHandle, }; diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index d4413ac68..383abfcb1 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -11,12 +11,12 @@ use std::path::Path; use async_trait::async_trait; -use liana::{ +use liana::miniscript::bitcoin::{ + address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, +}; +use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, - miniscript::bitcoin::{ - address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, - }, StartupError, }; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 072f4775b..582868d0c 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -3,11 +3,6 @@ use std::collections::{HashMap, HashSet}; use liana::descriptors::LianaDescriptor; pub use liana::{ - commands::{ - CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, - ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, - TransactionInfo, - }, descriptors::{LianaPolicy, PartialSpendInfo, PathSpendInfo}, miniscript::bitcoin::{ bip32::{DerivationPath, Fingerprint}, @@ -15,6 +10,10 @@ pub use liana::{ secp256k1, Address, Amount, Network, OutPoint, Transaction, Txid, }, }; +pub use lianad::commands::{ + CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry, + ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, +}; pub type Coin = ListCoinsEntry; diff --git a/liana-gui/src/installer/context.rs b/liana-gui/src/installer/context.rs index 3dab5185d..b33b8431e 100644 --- a/liana-gui/src/installer/context.rs +++ b/liana-gui/src/installer/context.rs @@ -9,11 +9,8 @@ use crate::{ signer::Signer, }; use async_hwi::DeviceKind; -use liana::{ - config::{BitcoinBackend, BitcoinConfig}, - descriptors::LianaDescriptor, - miniscript::bitcoin, -}; +use liana::{descriptors::LianaDescriptor, miniscript::bitcoin}; +use lianad::config::{BitcoinBackend, BitcoinConfig}; #[derive(Debug, Clone)] pub enum RemoteBackend { diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index f78f097b2..388369882 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -5,14 +5,12 @@ mod step; mod view; use iced::{clipboard, Command, Subscription}; -use liana::{ - config::Config, - miniscript::bitcoin::{self, Network}, -}; +use liana::miniscript::bitcoin::{self, Network}; use liana_ui::{ component::network_banner, widget::{Column, Element}, }; +use lianad::config::Config; use tracing::{error, info, warn}; use context::{Context, RemoteBackend}; @@ -332,9 +330,9 @@ impl Installer { } } -pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> { +pub fn daemon_check(cfg: lianad::config::Config) -> Result<(), Error> { // Start Daemon to check correctness of installation - match liana::DaemonHandle::start_default(cfg, false) { + match lianad::DaemonHandle::start_default(cfg, false) { Ok(daemon) => daemon .stop() .map_err(|e| Error::Unexpected(format!("Failed to stop Liana daemon: {}", e))), @@ -349,7 +347,7 @@ pub async fn install_local_wallet( ctx: Context, signer: Arc>, ) -> Result { - let mut cfg: liana::config::Config = extract_daemon_config(&ctx); + let mut cfg: lianad::config::Config = extract_daemon_config(&ctx); let data_dir = cfg.data_dir.unwrap(); let data_dir = data_dir diff --git a/liana-gui/src/installer/step/node/bitcoind.rs b/liana-gui/src/installer/step/node/bitcoind.rs index 16bd32c16..dd6ea627a 100644 --- a/liana-gui/src/installer/step/node/bitcoind.rs +++ b/liana-gui/src/installer/step/node/bitcoind.rs @@ -8,10 +8,8 @@ use bitcoin_hashes::{sha256, Hash}; #[cfg(any(target_os = "macos", target_os = "linux"))] use flate2::read::GzDecoder; use iced::{Command, Subscription}; -use liana::{ - config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth}, - miniscript::bitcoin::Network, -}; +use liana::miniscript::bitcoin::Network; +use lianad::config::{BitcoinBackend, BitcoindConfig, BitcoindRpcAuth}; #[cfg(any(target_os = "macos", target_os = "linux"))] use tar::Archive; use tracing::info; @@ -473,7 +471,7 @@ impl DefineBitcoind { } (Some(rpc_auth), Ok(addr)) => { ctx.bitcoin_backend = - Some(liana::config::BitcoinBackend::Bitcoind(BitcoindConfig { + Some(lianad::config::BitcoinBackend::Bitcoind(BitcoindConfig { rpc_auth, addr, })); diff --git a/liana-gui/src/installer/step/node/electrum.rs b/liana-gui/src/installer/step/node/electrum.rs index 96a43eefa..f594e6a22 100644 --- a/liana-gui/src/installer/step/node/electrum.rs +++ b/liana-gui/src/installer/step/node/electrum.rs @@ -1,9 +1,9 @@ use iced::Command; -use liana::{ +use liana_ui::{component::form, widget::*}; +use lianad::{ config::ElectrumConfig, electrum_client::{self, ElectrumApi}, }; -use liana_ui::{component::form, widget::*}; use crate::{ installer::{ @@ -45,7 +45,7 @@ impl DefineElectrum { pub fn apply(&mut self, ctx: &mut Context) -> bool { if self.can_try_ping() { - ctx.bitcoin_backend = Some(liana::config::BitcoinBackend::Electrum(ElectrumConfig { + ctx.bitcoin_backend = Some(lianad::config::BitcoinBackend::Electrum(ElectrumConfig { addr: self.address.value.clone(), })); return true; diff --git a/liana-gui/src/launcher.rs b/liana-gui/src/launcher.rs index f9ffe94bd..c9bbd7b88 100644 --- a/liana-gui/src/launcher.rs +++ b/liana-gui/src/launcher.rs @@ -6,13 +6,14 @@ use iced::{ Alignment, Command, Length, Subscription, }; -use liana::{config::ConfigError, miniscript::bitcoin::Network}; +use liana::miniscript::bitcoin::Network; use liana_ui::{ color, component::{button, card, modal::Modal, network_banner, notification, text::*}, icon, image, theme, widget::*, }; +use lianad::config::ConfigError; use crate::{app, installer::UserFlow}; @@ -466,7 +467,7 @@ async fn check_network_datadir(path: PathBuf, network: Network) -> Result { format!( diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index 8d4b33fc9..554df5548 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -9,11 +9,13 @@ use std::{ use async_trait::async_trait; use chrono::Utc; use liana::{ - commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem}, - config::Config, descriptors::LianaDescriptor, miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, }; +use lianad::{ + commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem}, + config::Config, +}; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; diff --git a/liana-gui/src/lib.rs b/liana-gui/src/lib.rs index e6ec9afca..a932e09e6 100644 --- a/liana-gui/src/lib.rs +++ b/liana-gui/src/lib.rs @@ -12,7 +12,7 @@ pub mod node; pub mod signer; pub mod utils; -use liana::Version; +use lianad::Version; pub const VERSION: Version = Version { major: 8, diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index 28a6dd5b2..48bc35526 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -9,18 +9,18 @@ use iced::{Alignment, Command, Length, Subscription}; use tokio::runtime::Handle; use tracing::{debug, info, warn}; -use liana::{ - commands::CoinStatus, - config::{BitcoinBackend, Config, ConfigError}, - miniscript::bitcoin, - StartupError, -}; +use liana::miniscript::bitcoin; use liana_ui::{ color, component::{button, notification, text::*}, icon, widget::*, }; +use lianad::{ + commands::CoinStatus, + config::{BitcoinBackend, Config, ConfigError}, + StartupError, +}; use crate::{ app::{ @@ -533,7 +533,7 @@ pub async fn start_bitcoind_and_daemon( if start_internal_bitcoind { if let Some(BitcoinBackend::Bitcoind(bitcoind_config)) = &config.bitcoin_backend { // Check if bitcoind is already running before trying to start it. - if liana::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok() + if lianad::BitcoinD::new(bitcoind_config, "internal_bitcoind_start".to_string()).is_ok() { info!("Internal bitcoind is already running"); } else { diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index c753a2c33..b9fb22457 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -17,8 +17,9 @@ use tracing_subscriber::filter::LevelFilter; extern crate serde; extern crate serde_json; -use liana::{config::Config as DaemonConfig, miniscript::bitcoin}; +use liana::miniscript::bitcoin; use liana_ui::{component::text, font, image, theme, widget::Element}; +use lianad::config::Config as DaemonConfig; use liana_gui::{ app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App}, diff --git a/liana-gui/src/node/bitcoind.rs b/liana-gui/src/node/bitcoind.rs index 0c818edbd..955c5898a 100644 --- a/liana-gui/src/node/bitcoind.rs +++ b/liana-gui/src/node/bitcoind.rs @@ -1,11 +1,11 @@ use base64::Engine; use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use liana::{ - config::BitcoindConfig, miniscript::bitcoin::{self, Network}, random::{random_bytes, RandomnessError}, }; use liana_ui::component::form; +use lianad::config::BitcoindConfig; use std::collections::BTreeMap; use std::fmt; use std::path::{Path, PathBuf}; @@ -462,7 +462,7 @@ impl Bitcoind { return Err(StartInternalBitcoindError::ProcessExited(status)); } } - match liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) { + match lianad::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) { Ok(_) => { log::info!("Bitcoind seems to have successfully started."); return Ok(Self { @@ -470,7 +470,7 @@ impl Bitcoind { _process: Arc::new(process), }); } - Err(liana::BitcoindError::CookieFile(_)) => { + Err(lianad::BitcoindError::CookieFile(_)) => { // This is only raised if we're using cookie authentication. // Assume cookie file has not been created yet and try again. } @@ -497,7 +497,7 @@ impl Bitcoind { } pub fn stop_bitcoind(config: &BitcoindConfig) -> bool { - match liana::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) { + match lianad::BitcoinD::new(config, "internal_bitcoind_stop".to_string()) { Ok(bitcoind) => { info!("Stopping internal bitcoind..."); bitcoind.stop(); diff --git a/liana-gui/src/node/mod.rs b/liana-gui/src/node/mod.rs index 70aabae33..35a7de26d 100644 --- a/liana-gui/src/node/mod.rs +++ b/liana-gui/src/node/mod.rs @@ -1,4 +1,4 @@ -use liana::config::BitcoinBackend; +use lianad::config::BitcoinBackend; pub mod bitcoind; pub mod electrum; diff --git a/liana/Cargo.toml b/liana/Cargo.toml index 79f7745d2..2b54116ac 100644 --- a/liana/Cargo.toml +++ b/liana/Cargo.toml @@ -6,19 +6,7 @@ edition = "2018" repository = "https://github.com/wizardsardine/liana" license-file = "LICENCE" keywords = ["bitcoin", "wallet", "miniscript", "inheritance", "recovery"] -description = "Liana wallet daemon" -exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pyproject.toml"] - -[[bin]] -name = "lianad" -path = "src/bin/daemon.rs" - -[[bin]] -name = "liana-cli" -path = "src/bin/cli.rs" - -[features] -nonblocking_shutdown = [] +description = "Liana development kit" [dependencies] # For managing transactions (it re-exports the bitcoin crate) @@ -26,34 +14,11 @@ miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] } # Coin selection algorithms for spend transaction creation. bdk_coin_select = "0.3" - -# For Electrum backend. This is the latest version with the same bitcoin version as -# the miniscript dependency. -bdk_electrum = { version = "0.14" } - -# Don't reinvent the wheel -dirs = "5.0" - # We use TOML for the config, and JSON for RPC serde = { version = "1.0", features = ["derive"] } -toml = "0.5" -serde_json = { version = "1.0", features = ["raw_value"] } # Logging stuff log = "0.4" -fern = "0.6" - -# In order to have a backtrace on panic, because the -# stdlib does not have a programmatic interface yet -# to work with our custom panic hook. -backtrace = "0.3" - -# Pinned to this version because they keep breaking their MSRV in point releases... -# FIXME: this is unfortunate, we don't receive the updates (sometimes critical) from SQLite. -rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] } - -# To talk to bitcoind -jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false } # Used for generating mnemonics getrandom = "0.2" diff --git a/liana/src/lib.rs b/liana/src/lib.rs index 23ed20cc9..60d01f746 100644 --- a/liana/src/lib.rs +++ b/liana/src/lib.rs @@ -1,874 +1,7 @@ -mod bitcoin; -pub mod commands; -pub mod config; -mod database; pub mod descriptors; -mod jsonrpc; pub mod random; pub mod signer; pub mod spend; -#[cfg(test)] -mod testutils; -pub use bdk_electrum::electrum_client; pub use bip39; -use bitcoin::electrum; pub use miniscript; - -pub use crate::bitcoin::{ - d::{BitcoinD, BitcoindError, WalletError}, - electrum::{Electrum, ElectrumError}, -}; - -use crate::jsonrpc::server; -use crate::{ - bitcoin::{poller, BitcoinInterface}, - config::Config, - database::{ - sqlite::{FreshDbOptions, SqliteDb, SqliteDbError, MAX_DB_VERSION_NO_TX_DB}, - DatabaseInterface, - }, -}; - -use std::{ - collections, error, fmt, fs, io, path, - sync::{self, mpsc}, - thread, -}; - -use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHash}; - -#[cfg(not(test))] -use std::panic; -// A panic in any thread should stop the main thread, and print the panic. -#[cfg(not(test))] -fn setup_panic_hook() { - panic::set_hook(Box::new(move |panic_info| { - let file = panic_info - .location() - .map(|l| l.file()) - .unwrap_or_else(|| "'unknown'"); - let line = panic_info - .location() - .map(|l| l.line().to_string()) - .unwrap_or_else(|| "'unknown'".to_string()); - - let bt = backtrace::Backtrace::new(); - let info = panic_info - .payload() - .downcast_ref::<&str>() - .map(|s| s.to_string()) - .or_else(|| panic_info.payload().downcast_ref::().cloned()); - log::error!( - "panic occurred at line {} of file {}: {:?}\n{:?}", - line, - file, - info, - bt - ); - })); -} - -#[derive(Debug, Clone)] -pub struct Version { - pub major: u32, - pub minor: u32, - pub patch: u32, -} - -impl fmt::Display for Version { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}.{}.{}-dev", self.major, self.minor, self.patch) - } -} - -pub const VERSION: Version = Version { - major: 8, - minor: 0, - patch: 0, -}; - -#[derive(Debug)] -pub enum StartupError { - Io(io::Error), - DefaultDataDirNotFound, - DatadirCreation(path::PathBuf, io::Error), - MissingBitcoindConfig, - MissingElectrumConfig, - MissingBitcoinBackendConfig, - DbMigrateBitcoinTxs(&'static str), - Database(SqliteDbError), - Bitcoind(BitcoindError), - Electrum(ElectrumError), - #[cfg(windows)] - NoWatchonlyInDatadir, -} - -impl fmt::Display for StartupError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Io(e) => write!(f, "{}", e), - Self::DefaultDataDirNotFound => write!( - f, - "Not data directory was specified and a default path could not be determined for this platform." - ), - Self::DatadirCreation(dir_path, e) => write!( - f, - "Could not create data directory at '{}': '{}'", dir_path.display(), e - ), - Self::MissingBitcoindConfig => write!( - f, - "Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration." - ), - Self::MissingElectrumConfig => write!( - f, - "Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration." - ), - Self::MissingBitcoinBackendConfig => write!( - f, - "No Bitcoin backend entry in the configuration." - ), - Self::DbMigrateBitcoinTxs(msg) => write!( - f, - "Error when migrating Bitcoin transaction from Bitcoin backend to database: {}.", msg - ), - Self::Database(e) => write!(f, "Error initializing database: '{}'.", e), - Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e), - Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e), - #[cfg(windows)] - Self::NoWatchonlyInDatadir => { - write!( - f, - "A data directory exists with no watchonly wallet. Really old versions of Liana used to not \ - store the bitcoind watchonly wallet under their own datadir on Windows. A migration will be \ - necessary to be able to use such an old datadir with recent versions of Liana. The migration \ - is automatically performed by Liana version 4 and older. If you want to salvage this datadir \ - first run Liana v4 before running more recent Liana versions." - ) - } - } - } -} - -impl error::Error for StartupError {} - -impl From for StartupError { - fn from(e: io::Error) -> Self { - Self::Io(e) - } -} - -impl From for StartupError { - fn from(e: SqliteDbError) -> Self { - Self::Database(e) - } -} - -impl From for StartupError { - fn from(e: BitcoindError) -> Self { - Self::Bitcoind(e) - } -} - -fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> { - #[cfg(unix)] - return { - use fs::DirBuilder; - use std::os::unix::fs::DirBuilderExt; - - let mut builder = DirBuilder::new(); - builder - .mode(0o700) - .recursive(true) - .create(datadir_path) - .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) - }; - - // TODO: permissions on Windows.. - #[cfg(not(unix))] - return { - fs::create_dir_all(datadir_path) - .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) - }; -} - -// Connect to the SQLite database. Create it if starting fresh, and do some sanity checks. -// If all went well, returns the interface to the SQLite database. -fn setup_sqlite( - config: &Config, - data_dir: &path::Path, - fresh_data_dir: bool, - secp: &secp256k1::Secp256k1, - bitcoind: &Option, -) -> Result { - let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")] - .iter() - .collect(); - let options = if fresh_data_dir { - Some(FreshDbOptions::new( - config.bitcoin_config.network, - config.main_descriptor.clone(), - )) - } else { - None - }; - - // If opening an existing wallet whose database does not yet store the wallet transactions, - // query them from the Bitcoin backend before proceeding to the migration. - let sqlite = SqliteDb::new(db_path, options, secp)?; - if !fresh_data_dir { - let mut conn = sqlite.connection()?; - let wallet_txs = if conn.db_version() <= MAX_DB_VERSION_NO_TX_DB { - let bit = bitcoind.as_ref().ok_or(StartupError::DbMigrateBitcoinTxs( - "a connection to a Bitcoin backend is required", - ))?; - let coins = conn.db_coins(&[]); - let coins_txids = coins - .iter() - .map(|c| c.outpoint.txid) - .chain(coins.iter().filter_map(|c| c.spend_txid)) - .collect::>(); - coins_txids - .into_iter() - .map(|txid| bit.get_transaction(&txid).map(|res| res.tx)) - .collect::>>() - .ok_or(StartupError::DbMigrateBitcoinTxs( - "missing transaction in Bitcoin backend", - ))? - } else { - Vec::new() - }; - sqlite.maybe_apply_migrations(&wallet_txs)?; - } - - sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?; - log::info!("Database initialized and checked."); - - Ok(sqlite) -} - -// Connect to bitcoind. Setup the watchonly wallet, and do some sanity checks. -// If all went well, returns the interface to bitcoind. -fn setup_bitcoind( - config: &Config, - data_dir: &path::Path, - fresh_data_dir: bool, -) -> Result { - let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")] - .iter() - .collect(); - let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string(); - // NOTE: On Windows, paths are canonicalized with a "\\?\" prefix to tell Windows to interpret - // the string "as is" and to ignore the maximum size of a path. HOWEVER this is not properly - // handled by most implementations of the C++ STL's std::filesystem. Therefore bitcoind would - // fail to find the wallet if we didn't strip this prefix. It's not ideal, but a lesser evil - // than other workarounds i could think about. - // See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces - // about the prefix. - // See https://stackoverflow.com/questions/71590689/how-to-properly-handle-windows-paths-with-the-long-path-prefix-with-stdfilesys - // for a discussion of how one C++ STL implementation handles this. - #[cfg(target_os = "windows")] - let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", ""); - - let bitcoind_config = match config.bitcoin_backend.as_ref() { - Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config, - _ => Err(StartupError::MissingBitcoindConfig)?, - }; - let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?; - bitcoind.node_sanity_checks( - config.bitcoin_config.network, - config.main_descriptor.is_taproot(), - )?; - if fresh_data_dir { - log::info!("Creating a new watchonly wallet on bitcoind."); - bitcoind.create_watchonly_wallet(&config.main_descriptor)?; - log::info!("Watchonly wallet created."); - } else { - #[cfg(windows)] - if !cfg!(test) && !wo_path.exists() { - return Err(StartupError::NoWatchonlyInDatadir); - } - } - log::info!("Loading our watchonly wallet on bitcoind."); - bitcoind.maybe_load_watchonly_wallet()?; - bitcoind.wallet_sanity_checks(&config.main_descriptor)?; - log::info!("Watchonly wallet loaded on bitcoind and sanity checked."); - - Ok(bitcoind) -} - -// Create an Electrum interface from a client and BDK-based wallet, and do some sanity checks. -// If all went well, returns the interface to Electrum. -fn setup_electrum( - config: &Config, - db: sync::Arc>, -) -> Result { - let electrum_config = match config.bitcoin_backend.as_ref() { - Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config, - _ => Err(StartupError::MissingElectrumConfig)?, - }; - // First create the client to communicate with the Electrum server. - let client = electrum::client::Client::new(electrum_config) - .map_err(|e| StartupError::Electrum(ElectrumError::Client(e)))?; - // Then create the BDK-based wallet and populate it with DB data. - let mut db_conn = db.connection(); - let tip = db_conn.chain_tip(); - let coins: Vec<_> = db_conn - .coins(&[], &[]) - .into_values() - .map(|c| crate::bitcoin::Coin { - outpoint: c.outpoint, - amount: c.amount, - derivation_index: c.derivation_index, - is_change: c.is_change, - is_immature: c.is_immature, - block_info: c.block_info.map(|info| crate::bitcoin::BlockInfo { - height: info.height, - time: info.time, - }), - spend_txid: c.spend_txid, - spend_block: c.spend_block.map(|info| crate::bitcoin::BlockInfo { - height: info.height, - time: info.time, - }), - }) - .collect(); - let txids = db_conn.list_saved_txids(); - // This will only return those txs referenced by our coins, which may not be all of `txids`. - let txs: Vec<_> = db_conn - .list_wallet_transactions(&txids) - .into_iter() - .map(|(tx, _, _)| tx) - .collect(); - let (receive_index, change_index) = (db_conn.receive_index(), db_conn.change_index()); - let genesis_hash = { - let chain_hash = ChainHash::using_genesis_block(config.bitcoin_config.network); - BlockHash::from_byte_array(*chain_hash.as_bytes()) - }; - let bdk_wallet = electrum::wallet::BdkWallet::new( - &config.main_descriptor, - genesis_hash, - tip, - &coins, - &txs, - receive_index, - change_index, - ); - let full_scan = db_conn.rescan_timestamp().is_some(); - let electrum = Electrum::new(client, bdk_wallet, full_scan).map_err(StartupError::Electrum)?; - electrum - .sanity_checks(&genesis_hash) - .map_err(StartupError::Electrum)?; - Ok(electrum) -} - -#[derive(Clone)] -pub struct DaemonControl { - config: Config, - bitcoin: sync::Arc>, - poller_sender: mpsc::SyncSender, - // FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex? - db: sync::Arc>, - secp: secp256k1::Secp256k1, -} - -impl DaemonControl { - pub(crate) fn new( - config: Config, - bitcoin: sync::Arc>, - poller_sender: mpsc::SyncSender, - db: sync::Arc>, - secp: secp256k1::Secp256k1, - ) -> DaemonControl { - DaemonControl { - config, - bitcoin, - poller_sender, - db, - secp, - } - } - - // Useful for unit test to directly mess up with the DB - #[cfg(test)] - pub fn db(&self) -> sync::Arc> { - self.db.clone() - } -} - -/// The handle to a Liana daemon. It might either be the handle for a daemon which exposes a -/// JSONRPC server or one which exposes its API through a `DaemonControl`. -pub enum DaemonHandle { - Controller { - poller_sender: mpsc::SyncSender, - poller_handle: thread::JoinHandle<()>, - control: DaemonControl, - }, - Server { - poller_sender: mpsc::SyncSender, - poller_handle: thread::JoinHandle<()>, - rpcserver_shutdown: sync::Arc, - rpcserver_handle: thread::JoinHandle>, - }, -} - -impl DaemonHandle { - /// This starts the Liana daemon. A user of this interface should regularly poll the `is_alive` - /// method to check for internal errors. To shut down the daemon use the `stop` method. - /// - /// The `with_rpc_server` controls whether we should start a JSONRPC server to receive queries - /// or instead return a `DaemonControl` object for a caller to access the daemon's API. - /// - /// You may specify a custom Bitcoin interface through the `bitcoin` parameter. If `None`, the - /// default Bitcoin interface (`bitcoind` JSONRPC) will be used. - /// You may specify a custom Database interface through the `db` parameter. If `None`, the - /// default Database interface (SQLite) will be used. - pub fn start( - config: Config, - bitcoin: Option, - db: Option, - with_rpc_server: bool, - ) -> Result { - #[cfg(not(test))] - setup_panic_hook(); - - let secp = secp256k1::Secp256k1::verification_only(); - - // First, check the data directory - let mut data_dir = config - .data_dir() - .ok_or(StartupError::DefaultDataDirNotFound)?; - data_dir.push(config.bitcoin_config.network.to_string()); - let fresh_data_dir = !data_dir.as_path().exists(); - if fresh_data_dir { - create_datadir(&data_dir)?; - log::info!("Created a new data directory at '{}'", data_dir.display()); - } - - // Set up the connection to bitcoind (if using it) first as we may need it for the database - // migration when setting up SQLite below. - let bitcoind = if bitcoin.is_none() { - if let Some(config::BitcoinBackend::Bitcoind(_)) = &config.bitcoin_backend { - Some(setup_bitcoind(&config, &data_dir, fresh_data_dir)?) - } else { - None - } - } else { - None - }; - - // Then set up the database backend. - let db = match db { - Some(db) => sync::Arc::from(sync::Mutex::from(db)), - None => sync::Arc::from(sync::Mutex::from(setup_sqlite( - &config, - &data_dir, - fresh_data_dir, - &secp, - &bitcoind, - )?)) as sync::Arc>, - }; - - // Finally set up the Bitcoin backend. - let bit = match (bitcoin, &config.bitcoin_backend) { - (Some(bit), _) => sync::Arc::from(sync::Mutex::from(bit)), - (None, Some(config::BitcoinBackend::Bitcoind(..))) => sync::Arc::from( - sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")), - ) - as sync::Arc>, - (None, Some(config::BitcoinBackend::Electrum(..))) => { - sync::Arc::from(sync::Mutex::from(setup_electrum(&config, db.clone())?)) - } - (None, None) => Err(StartupError::MissingBitcoinBackendConfig)?, - }; - - // Start the poller thread. Keep the thread handle to be able to check if it crashed. Store - // an atomic to be able to stop it. - let mut bitcoin_poller = - poller::Poller::new(bit.clone(), db.clone(), config.main_descriptor.clone()); - let (poller_sender, poller_receiver) = mpsc::sync_channel(0); - let poller_handle = thread::Builder::new() - .name("Bitcoin Network poller".to_string()) - .spawn({ - let poll_interval = config.bitcoin_config.poll_interval_secs; - move || { - log::info!("Bitcoin poller started."); - bitcoin_poller.poll_forever(poll_interval, poller_receiver); - log::info!("Bitcoin poller stopped."); - } - }) - .expect("Spawning the poller thread must never fail."); - - // Create the API the external world will use to talk to us, either directly through the Rust - // structure or through the JSONRPC server we may setup below. - let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp); - - if with_rpc_server { - let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false)); - let rpcserver_handle = thread::Builder::new() - .name("Bitcoin Network poller".to_string()) - .spawn({ - let shutdown = rpcserver_shutdown.clone(); - move || { - let mut rpc_socket = data_dir; - rpc_socket.push("lianad_rpc"); - server::run(&rpc_socket, control, shutdown)?; - Ok(()) - } - }) - .expect("Spawning the RPC server thread should never fail."); - - return Ok(DaemonHandle::Server { - poller_sender, - poller_handle, - rpcserver_shutdown, - rpcserver_handle, - }); - } - - Ok(DaemonHandle::Controller { - poller_sender, - poller_handle, - control, - }) - } - - /// Start the Liana daemon with the default Bitcoin and database interfaces (`bitcoind` RPC - /// and SQLite). - pub fn start_default( - config: Config, - with_rpc_server: bool, - ) -> Result { - Self::start( - config, - Option::::None, - Option::::None, - with_rpc_server, - ) - } - - /// Check whether the daemon is still up and running. This needs to be regularly polled to - /// check for internal errors. If this returns `false`, collect the error using the `stop` - /// method. - pub fn is_alive(&self) -> bool { - match self { - Self::Controller { - ref poller_handle, .. - } => !poller_handle.is_finished(), - Self::Server { - ref poller_handle, - ref rpcserver_handle, - .. - } => !poller_handle.is_finished() && !rpcserver_handle.is_finished(), - } - } - - /// Stop the Liana daemon. This returns any error which may have occurred. - pub fn stop(self) -> Result<(), Box> { - match self { - Self::Controller { - poller_sender, - poller_handle, - .. - } => { - poller_sender - .send(poller::PollerMessage::Shutdown) - .expect("The other end should never have hung up before this."); - poller_handle.join().expect("Poller thread must not panic"); - Ok(()) - } - Self::Server { - poller_sender, - poller_handle, - rpcserver_shutdown, - rpcserver_handle, - } => { - poller_sender - .send(poller::PollerMessage::Shutdown) - .expect("The other end should never have hung up before this."); - rpcserver_shutdown.store(true, sync::atomic::Ordering::Relaxed); - rpcserver_handle - .join() - .expect("Poller thread must not panic")?; - poller_handle.join().expect("Poller thread must not panic"); - Ok(()) - } - } - } -} - -#[cfg(all(test, unix))] -mod tests { - use super::*; - use crate::{ - config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth}, - descriptors::LianaDescriptor, - testutils::*, - }; - - use miniscript::bitcoin; - use std::{ - fs, - io::{BufRead, BufReader, Write}, - net, path, - str::FromStr, - thread, time, - }; - - // Read all bytes from the socket until the end of a JSON object, good enough approximation. - fn read_til_json_end(stream: &mut net::TcpStream) { - stream - .set_read_timeout(Some(time::Duration::from_secs(5))) - .unwrap(); - let mut reader = BufReader::new(stream); - loop { - let mut line = String::new(); - reader.read_line(&mut line).unwrap(); - - if line.starts_with("Authorization") { - let mut buf = vec![0; 256]; - reader.read_until(b'}', &mut buf).unwrap(); - return; - } - } - } - - // Respond to the two "echo" sent at startup to sanity check the connection - fn complete_sanity_check(server: &net::TcpListener) { - let echo_resp = - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes(); - - // Read the first echo, respond to it - { - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(echo_resp).unwrap(); - stream.flush().unwrap(); - } - - // Read the second echo, respond to it - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(echo_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a pruned getblockchaininfo telling them we are at version 24.0 - fn complete_version_check(server: &net::TcpListener) { - let net_resp = - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":240000}}\n" - .as_bytes(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(net_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a pruned getblockchaininfo telling them we are on mainnet - fn complete_network_check(server: &net::TcpListener) { - let net_resp = - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"chain\":\"main\"}}\n" - .as_bytes(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(net_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them responses for the calls involved when creating a fresh wallet - fn complete_wallet_creation(server: &net::TcpListener) { - { - let net_resp = - ["HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes()] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - { - let net_resp = [ - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n" - .as_bytes(), - ] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - let net_resp = [ - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[{\"success\":true}]}\n" - .as_bytes(), - ] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a dummy result to loadwallet. - fn complete_wallet_loading(server: &net::TcpListener) { - { - let listwallets_resp = - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(listwallets_resp).unwrap(); - stream.flush().unwrap(); - } - - let loadwallet_resp = - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n" - .as_bytes(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(loadwallet_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a response to 'listwallets' with the watchonly wallet path - fn complete_wallet_check(server: &net::TcpListener, watchonly_wallet_path: &str) { - let net_resp = [ - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"".as_bytes(), - watchonly_wallet_path.as_bytes(), - "\"]}\n".as_bytes(), - ] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a response to 'listdescriptors' with the receive and change descriptors - fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) { - let net_resp = [ - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(), - receive_desc.as_bytes(), - "\",\"timestamp\":0},".as_bytes(), - "{\"desc\":\"".as_bytes(), - change_desc.as_bytes(), - "\",\"timestamp\":1}]}}\n".as_bytes(), - ] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - // Send them a response to 'getblockhash' with the genesis block hash - fn complete_tip_init(server: &net::TcpListener) { - let net_resp = [ - "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"}\n".as_bytes(), - ] - .concat(); - let (mut stream, _) = server.accept().unwrap(); - read_til_json_end(&mut stream); - stream.write_all(&net_resp).unwrap(); - stream.flush().unwrap(); - } - - // TODO: we could move the dummy bitcoind thread stuff to the bitcoind module to test the - // bitcoind interface, and use the DummyLiana from testutils to sanity check the startup. - // Note that startup as checked by this unit test is also tested in the functional test - // framework. - #[test] - fn daemon_startup() { - let tmp_dir = tmp_dir(); - fs::create_dir_all(&tmp_dir).unwrap(); - let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")] - .iter() - .collect(); - let wo_path: path::PathBuf = [ - data_dir.as_path(), - path::Path::new("bitcoin"), - path::Path::new("lianad_watchonly_wallet"), - ] - .iter() - .collect(); - let wo_path = wo_path.to_str().unwrap().to_string(); - - // Configure a dummy bitcoind - let network = bitcoin::Network::Bitcoin; - let cookie: path::PathBuf = [ - tmp_dir.as_path(), - path::Path::new(&format!( - "dummy_bitcoind_{:?}.cookie", - thread::current().id() - )), - ] - .iter() - .collect(); - fs::write(&cookie, [0; 32]).unwrap(); // Will overwrite should it exist already - let addr: net::SocketAddr = - net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into(); - let server = net::TcpListener::bind(addr).unwrap(); - let addr = server.local_addr().unwrap(); - let bitcoin_config = BitcoinConfig { - network, - poll_interval_secs: time::Duration::from_secs(2), - }; - let bitcoind_config = BitcoindConfig { - addr, - rpc_auth: BitcoindRpcAuth::CookieFile(cookie), - }; - - // Create a dummy config with this bitcoind - let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn"; - let desc = LianaDescriptor::from_str(desc_str).unwrap(); - let receive_desc = desc.receive_descriptor().clone(); - let change_desc = desc.change_descriptor().clone(); - let config = Config { - bitcoin_config, - bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)), - data_dir: Some(data_dir), - log_level: log::LevelFilter::Debug, - main_descriptor: desc, - }; - - // Start the daemon in a new thread so the current one acts as the bitcoind server. - let t = thread::spawn({ - let config = config.clone(); - move || { - let handle = DaemonHandle::start_default(config, false).unwrap(); - handle.stop().unwrap(); - } - }); - complete_sanity_check(&server); - complete_version_check(&server); - complete_network_check(&server); - complete_wallet_creation(&server); - complete_wallet_loading(&server); - complete_wallet_check(&server, &wo_path); - complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string()); - complete_tip_init(&server); - // We don't have to complete the sync check as the poller checks whether it needs to stop - // before checking the bitcoind sync status. - t.join().unwrap(); - - // The datadir is created now, so if we restart it it won't create the wo wallet. - let t = thread::spawn({ - let config = config.clone(); - move || { - let handle = DaemonHandle::start_default(config, false).unwrap(); - handle.stop().unwrap(); - } - }); - complete_sanity_check(&server); - complete_version_check(&server); - complete_network_check(&server); - complete_wallet_loading(&server); - complete_wallet_check(&server, &wo_path); - complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string()); - // We don't have to complete the sync check as the poller checks whether it needs to stop - // before checking the bitcoind sync status. - t.join().unwrap(); - - fs::remove_dir_all(&tmp_dir).unwrap(); - } -} diff --git a/liana/src/signer.rs b/liana/src/signer.rs index 1d4601348..a542b116e 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -407,13 +407,30 @@ impl HotSigner { #[cfg(test)] mod tests { use super::*; - use crate::{descriptors, testutils::*}; + use crate::descriptors; use miniscript::{ bitcoin::{locktime::absolute, psbt::Input as PsbtIn, Amount}, descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard}, }; use std::collections::{BTreeMap, HashSet}; + static mut COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + fn uid() -> usize { + unsafe { + let uid = COUNTER.load(std::sync::atomic::Ordering::Relaxed); + COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + uid + } + } + fn tmp_dir() -> path::PathBuf { + std::env::temp_dir().join(format!( + "lianad-{}-{:?}-{}", + std::process::id(), + std::thread::current().id(), + uid(), + )) + } + #[test] fn hot_signer_gen() { // Entropy isn't completely broken. diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml new file mode 100644 index 000000000..f051374d8 --- /dev/null +++ b/lianad/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "lianad" +version = "8.0.0" +authors = ["Antoine Poinsot "] +edition = "2018" +repository = "https://github.com/wizardsardine/liana" +license-file = "LICENCE" +keywords = ["bitcoin", "wallet", "miniscript", "inheritance", "recovery"] +description = "Liana wallet daemon" +exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pyproject.toml"] + +[[bin]] +name = "lianad" +path = "src/bin/daemon.rs" + +[[bin]] +name = "liana-cli" +path = "src/bin/cli.rs" + +[features] +nonblocking_shutdown = [] + +[dependencies] +liana = { path = "../liana" } +# For managing transactions (it re-exports the bitcoin crate) +miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] } + +# For Electrum backend. This is the latest version with the same bitcoin version as +# the miniscript dependency. +bdk_electrum = { version = "0.14" } + +# Don't reinvent the wheel +dirs = "5.0" + +# We use TOML for the config, and JSON for RPC +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" +serde_json = { version = "1.0", features = ["raw_value"] } + +# Logging stuff +log = "0.4" +fern = "0.6" + +# In order to have a backtrace on panic, because the +# stdlib does not have a programmatic interface yet +# to work with our custom panic hook. +backtrace = "0.3" + +# Pinned to this version because they keep breaking their MSRV in point releases... +# FIXME: this is unfortunate, we don't receive the updates (sometimes critical) from SQLite. +rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] } + +# To talk to bitcoind +jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false } diff --git a/liana/src/bin/cli.rs b/lianad/src/bin/cli.rs similarity index 99% rename from liana/src/bin/cli.rs rename to lianad/src/bin/cli.rs index 91c1459f0..60ae9937f 100644 --- a/liana/src/bin/cli.rs +++ b/lianad/src/bin/cli.rs @@ -1,6 +1,6 @@ #![cfg(not(target_os = "windows"))] -use liana::config::{config_folder_path, Config}; +use lianad::config::{config_folder_path, Config}; use std::{ env, diff --git a/liana/src/bin/daemon.rs b/lianad/src/bin/daemon.rs similarity index 98% rename from liana/src/bin/daemon.rs rename to lianad/src/bin/daemon.rs index 0b050d920..7b1ffbaa2 100644 --- a/liana/src/bin/daemon.rs +++ b/lianad/src/bin/daemon.rs @@ -5,7 +5,7 @@ use std::{ process, thread, time, }; -use liana::{config::Config, DaemonHandle, VERSION}; +use lianad::{config::Config, DaemonHandle, VERSION}; fn print_help_exit(code: i32) { eprintln!("lianad version {}", VERSION); diff --git a/liana/src/bitcoin/d/mod.rs b/lianad/src/bitcoin/d/mod.rs similarity index 99% rename from liana/src/bitcoin/d/mod.rs rename to lianad/src/bitcoin/d/mod.rs index b92625381..a7b088a33 100644 --- a/liana/src/bitcoin/d/mod.rs +++ b/lianad/src/bitcoin/d/mod.rs @@ -6,8 +6,8 @@ mod utils; use crate::{ bitcoin::{Block, BlockChainTip}, config, - descriptors::LianaDescriptor, }; +use liana::descriptors::LianaDescriptor; use utils::{block_before_date, roundup_progress}; use std::{ diff --git a/liana/src/bitcoin/d/utils.rs b/lianad/src/bitcoin/d/utils.rs similarity index 100% rename from liana/src/bitcoin/d/utils.rs rename to lianad/src/bitcoin/d/utils.rs diff --git a/liana/src/bitcoin/electrum/client.rs b/lianad/src/bitcoin/electrum/client.rs similarity index 100% rename from liana/src/bitcoin/electrum/client.rs rename to lianad/src/bitcoin/electrum/client.rs diff --git a/liana/src/bitcoin/electrum/mod.rs b/lianad/src/bitcoin/electrum/mod.rs similarity index 100% rename from liana/src/bitcoin/electrum/mod.rs rename to lianad/src/bitcoin/electrum/mod.rs diff --git a/liana/src/bitcoin/electrum/utils.rs b/lianad/src/bitcoin/electrum/utils.rs similarity index 100% rename from liana/src/bitcoin/electrum/utils.rs rename to lianad/src/bitcoin/electrum/utils.rs diff --git a/liana/src/bitcoin/electrum/wallet.rs b/lianad/src/bitcoin/electrum/wallet.rs similarity index 99% rename from liana/src/bitcoin/electrum/wallet.rs rename to lianad/src/bitcoin/electrum/wallet.rs index 116b85161..013a05641 100644 --- a/liana/src/bitcoin/electrum/wallet.rs +++ b/lianad/src/bitcoin/electrum/wallet.rs @@ -17,10 +17,8 @@ use miniscript::bitcoin::bip32::ChildNumber; use super::utils::{ block_id_from_tip, block_info_from_anchor, height_i32_from_u32, height_u32_from_i32, }; -use crate::{ - bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY}, - descriptors::LianaDescriptor, -}; +use crate::bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY}; +use liana::descriptors::LianaDescriptor; // We don't want to overload the server (each SPK is separate call). const LOOK_AHEAD_LIMIT: u32 = 30; diff --git a/liana/src/bitcoin/mod.rs b/lianad/src/bitcoin/mod.rs similarity index 99% rename from liana/src/bitcoin/mod.rs rename to lianad/src/bitcoin/mod.rs index 104d80d6f..c4c328bc5 100644 --- a/liana/src/bitcoin/mod.rs +++ b/lianad/src/bitcoin/mod.rs @@ -6,11 +6,9 @@ pub mod d; pub mod electrum; pub mod poller; -use crate::{ - bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry}, - descriptors, -}; +use crate::bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry}; pub use d::{MempoolEntry, MempoolEntryFees, SyncProgress}; +use liana::descriptors; use std::{fmt, sync}; diff --git a/liana/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs similarity index 99% rename from liana/src/bitcoin/poller/looper.rs rename to lianad/src/bitcoin/poller/looper.rs index 8c797d092..baa95c2cf 100644 --- a/liana/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -1,11 +1,11 @@ use crate::{ bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress}, database::{Coin, DatabaseConnection, DatabaseInterface}, - descriptors, }; use std::{collections::HashSet, convert::TryInto, sync, thread, time}; +use liana::descriptors; use miniscript::bitcoin::{self, secp256k1}; #[derive(Debug, Clone)] diff --git a/liana/src/bitcoin/poller/mod.rs b/lianad/src/bitcoin/poller/mod.rs similarity index 99% rename from liana/src/bitcoin/poller/mod.rs rename to lianad/src/bitcoin/poller/mod.rs index b23043943..bbc11933e 100644 --- a/liana/src/bitcoin/poller/mod.rs +++ b/lianad/src/bitcoin/poller/mod.rs @@ -1,6 +1,7 @@ mod looper; -use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface, descriptors}; +use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface}; +use liana::descriptors; use std::{ sync::{self, mpsc}, diff --git a/liana/src/commands/mod.rs b/lianad/src/commands/mod.rs similarity index 99% rename from liana/src/commands/mod.rs rename to lianad/src/commands/mod.rs index bfdce741f..eba110d7d 100644 --- a/liana/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -7,18 +7,21 @@ mod utils; use crate::{ bitcoin::BitcoinInterface, database::{Coin, DatabaseConnection, DatabaseInterface}, - descriptors, miniscript::bitcoin::absolute::LockTime, poller::PollerMessage, + DaemonControl, VERSION, +}; + +pub use crate::database::{CoinStatus, LabelItem}; + +use liana::{ + descriptors, spend::{ self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes, SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter, }, - DaemonControl, VERSION, }; -pub use crate::database::{CoinStatus, LabelItem}; - use utils::{ deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount, ser_hex, ser_to_string, @@ -1280,7 +1283,8 @@ pub struct CreateRecoveryResult { #[cfg(test)] mod tests { use super::*; - use crate::{bitcoin::Block, database::BlockInfo, spend::InsaneFeeInfo, testutils::*}; + use crate::{bitcoin::Block, database::BlockInfo, testutils::*}; + use liana::spend::InsaneFeeInfo; use bitcoin::{ bip32::{self, ChildNumber}, diff --git a/liana/src/commands/utils.rs b/lianad/src/commands/utils.rs similarity index 100% rename from liana/src/commands/utils.rs rename to lianad/src/commands/utils.rs diff --git a/liana/src/config.rs b/lianad/src/config.rs similarity index 99% rename from liana/src/config.rs rename to lianad/src/config.rs index 2399e161b..bd8d19948 100644 --- a/liana/src/config.rs +++ b/lianad/src/config.rs @@ -1,4 +1,4 @@ -use crate::descriptors::LianaDescriptor; +use liana::descriptors::LianaDescriptor; use std::{fmt, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration}; diff --git a/liana/src/database/mod.rs b/lianad/src/database/mod.rs similarity index 100% rename from liana/src/database/mod.rs rename to lianad/src/database/mod.rs diff --git a/liana/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs similarity index 99% rename from liana/src/database/sqlite/mod.rs rename to lianad/src/database/sqlite/mod.rs index 7fbafb3cd..27676862b 100644 --- a/liana/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -25,8 +25,8 @@ use crate::{ }, Coin, CoinStatus, LabelItem, }, - descriptors::LianaDescriptor, }; +use liana::descriptors::LianaDescriptor; use std::{ cmp, diff --git a/liana/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs similarity index 99% rename from liana/src/database/sqlite/schema.rs rename to lianad/src/database/sqlite/schema.rs index 57d01a0c3..52a83a0be 100644 --- a/liana/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -1,4 +1,4 @@ -use crate::descriptors::LianaDescriptor; +use liana::descriptors::LianaDescriptor; use std::{convert::TryFrom, str::FromStr}; diff --git a/liana/src/database/sqlite/utils.rs b/lianad/src/database/sqlite/utils.rs similarity index 100% rename from liana/src/database/sqlite/utils.rs rename to lianad/src/database/sqlite/utils.rs diff --git a/liana/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs similarity index 100% rename from liana/src/jsonrpc/api.rs rename to lianad/src/jsonrpc/api.rs diff --git a/liana/src/jsonrpc/mod.rs b/lianad/src/jsonrpc/mod.rs similarity index 100% rename from liana/src/jsonrpc/mod.rs rename to lianad/src/jsonrpc/mod.rs diff --git a/liana/src/jsonrpc/rpc.rs b/lianad/src/jsonrpc/rpc.rs similarity index 100% rename from liana/src/jsonrpc/rpc.rs rename to lianad/src/jsonrpc/rpc.rs diff --git a/liana/src/jsonrpc/server/mod.rs b/lianad/src/jsonrpc/server/mod.rs similarity index 100% rename from liana/src/jsonrpc/server/mod.rs rename to lianad/src/jsonrpc/server/mod.rs diff --git a/liana/src/jsonrpc/server/unix.rs b/lianad/src/jsonrpc/server/unix.rs similarity index 100% rename from liana/src/jsonrpc/server/unix.rs rename to lianad/src/jsonrpc/server/unix.rs diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs new file mode 100644 index 000000000..5c7e3f814 --- /dev/null +++ b/lianad/src/lib.rs @@ -0,0 +1,870 @@ +mod bitcoin; +pub mod commands; +pub mod config; +mod database; +mod jsonrpc; +#[cfg(test)] +mod testutils; + +pub use bdk_electrum::electrum_client; +use bitcoin::electrum; +pub use miniscript; + +pub use crate::bitcoin::{ + d::{BitcoinD, BitcoindError, WalletError}, + electrum::{Electrum, ElectrumError}, +}; + +use crate::jsonrpc::server; +use crate::{ + bitcoin::{poller, BitcoinInterface}, + config::Config, + database::{ + sqlite::{FreshDbOptions, SqliteDb, SqliteDbError, MAX_DB_VERSION_NO_TX_DB}, + DatabaseInterface, + }, +}; + +use std::{ + collections, error, fmt, fs, io, path, + sync::{self, mpsc}, + thread, +}; + +use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHash}; + +#[cfg(not(test))] +use std::panic; +// A panic in any thread should stop the main thread, and print the panic. +#[cfg(not(test))] +fn setup_panic_hook() { + panic::set_hook(Box::new(move |panic_info| { + let file = panic_info + .location() + .map(|l| l.file()) + .unwrap_or_else(|| "'unknown'"); + let line = panic_info + .location() + .map(|l| l.line().to_string()) + .unwrap_or_else(|| "'unknown'".to_string()); + + let bt = backtrace::Backtrace::new(); + let info = panic_info + .payload() + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()); + log::error!( + "panic occurred at line {} of file {}: {:?}\n{:?}", + line, + file, + info, + bt + ); + })); +} + +#[derive(Debug, Clone)] +pub struct Version { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}.{}-dev", self.major, self.minor, self.patch) + } +} + +pub const VERSION: Version = Version { + major: 8, + minor: 0, + patch: 0, +}; + +#[derive(Debug)] +pub enum StartupError { + Io(io::Error), + DefaultDataDirNotFound, + DatadirCreation(path::PathBuf, io::Error), + MissingBitcoindConfig, + MissingElectrumConfig, + MissingBitcoinBackendConfig, + DbMigrateBitcoinTxs(&'static str), + Database(SqliteDbError), + Bitcoind(BitcoindError), + Electrum(ElectrumError), + #[cfg(windows)] + NoWatchonlyInDatadir, +} + +impl fmt::Display for StartupError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Io(e) => write!(f, "{}", e), + Self::DefaultDataDirNotFound => write!( + f, + "Not data directory was specified and a default path could not be determined for this platform." + ), + Self::DatadirCreation(dir_path, e) => write!( + f, + "Could not create data directory at '{}': '{}'", dir_path.display(), e + ), + Self::MissingBitcoindConfig => write!( + f, + "Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration." + ), + Self::MissingElectrumConfig => write!( + f, + "Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration." + ), + Self::MissingBitcoinBackendConfig => write!( + f, + "No Bitcoin backend entry in the configuration." + ), + Self::DbMigrateBitcoinTxs(msg) => write!( + f, + "Error when migrating Bitcoin transaction from Bitcoin backend to database: {}.", msg + ), + Self::Database(e) => write!(f, "Error initializing database: '{}'.", e), + Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e), + Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e), + #[cfg(windows)] + Self::NoWatchonlyInDatadir => { + write!( + f, + "A data directory exists with no watchonly wallet. Really old versions of Liana used to not \ + store the bitcoind watchonly wallet under their own datadir on Windows. A migration will be \ + necessary to be able to use such an old datadir with recent versions of Liana. The migration \ + is automatically performed by Liana version 4 and older. If you want to salvage this datadir \ + first run Liana v4 before running more recent Liana versions." + ) + } + } + } +} + +impl error::Error for StartupError {} + +impl From for StartupError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +impl From for StartupError { + fn from(e: SqliteDbError) -> Self { + Self::Database(e) + } +} + +impl From for StartupError { + fn from(e: BitcoindError) -> Self { + Self::Bitcoind(e) + } +} + +fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> { + #[cfg(unix)] + return { + use fs::DirBuilder; + use std::os::unix::fs::DirBuilderExt; + + let mut builder = DirBuilder::new(); + builder + .mode(0o700) + .recursive(true) + .create(datadir_path) + .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) + }; + + // TODO: permissions on Windows.. + #[cfg(not(unix))] + return { + fs::create_dir_all(datadir_path) + .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e)) + }; +} + +// Connect to the SQLite database. Create it if starting fresh, and do some sanity checks. +// If all went well, returns the interface to the SQLite database. +fn setup_sqlite( + config: &Config, + data_dir: &path::Path, + fresh_data_dir: bool, + secp: &secp256k1::Secp256k1, + bitcoind: &Option, +) -> Result { + let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")] + .iter() + .collect(); + let options = if fresh_data_dir { + Some(FreshDbOptions::new( + config.bitcoin_config.network, + config.main_descriptor.clone(), + )) + } else { + None + }; + + // If opening an existing wallet whose database does not yet store the wallet transactions, + // query them from the Bitcoin backend before proceeding to the migration. + let sqlite = SqliteDb::new(db_path, options, secp)?; + if !fresh_data_dir { + let mut conn = sqlite.connection()?; + let wallet_txs = if conn.db_version() <= MAX_DB_VERSION_NO_TX_DB { + let bit = bitcoind.as_ref().ok_or(StartupError::DbMigrateBitcoinTxs( + "a connection to a Bitcoin backend is required", + ))?; + let coins = conn.db_coins(&[]); + let coins_txids = coins + .iter() + .map(|c| c.outpoint.txid) + .chain(coins.iter().filter_map(|c| c.spend_txid)) + .collect::>(); + coins_txids + .into_iter() + .map(|txid| bit.get_transaction(&txid).map(|res| res.tx)) + .collect::>>() + .ok_or(StartupError::DbMigrateBitcoinTxs( + "missing transaction in Bitcoin backend", + ))? + } else { + Vec::new() + }; + sqlite.maybe_apply_migrations(&wallet_txs)?; + } + + sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?; + log::info!("Database initialized and checked."); + + Ok(sqlite) +} + +// Connect to bitcoind. Setup the watchonly wallet, and do some sanity checks. +// If all went well, returns the interface to bitcoind. +fn setup_bitcoind( + config: &Config, + data_dir: &path::Path, + fresh_data_dir: bool, +) -> Result { + let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")] + .iter() + .collect(); + let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string(); + // NOTE: On Windows, paths are canonicalized with a "\\?\" prefix to tell Windows to interpret + // the string "as is" and to ignore the maximum size of a path. HOWEVER this is not properly + // handled by most implementations of the C++ STL's std::filesystem. Therefore bitcoind would + // fail to find the wallet if we didn't strip this prefix. It's not ideal, but a lesser evil + // than other workarounds i could think about. + // See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces + // about the prefix. + // See https://stackoverflow.com/questions/71590689/how-to-properly-handle-windows-paths-with-the-long-path-prefix-with-stdfilesys + // for a discussion of how one C++ STL implementation handles this. + #[cfg(target_os = "windows")] + let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", ""); + + let bitcoind_config = match config.bitcoin_backend.as_ref() { + Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config, + _ => Err(StartupError::MissingBitcoindConfig)?, + }; + let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?; + bitcoind.node_sanity_checks( + config.bitcoin_config.network, + config.main_descriptor.is_taproot(), + )?; + if fresh_data_dir { + log::info!("Creating a new watchonly wallet on bitcoind."); + bitcoind.create_watchonly_wallet(&config.main_descriptor)?; + log::info!("Watchonly wallet created."); + } else { + #[cfg(windows)] + if !cfg!(test) && !wo_path.exists() { + return Err(StartupError::NoWatchonlyInDatadir); + } + } + log::info!("Loading our watchonly wallet on bitcoind."); + bitcoind.maybe_load_watchonly_wallet()?; + bitcoind.wallet_sanity_checks(&config.main_descriptor)?; + log::info!("Watchonly wallet loaded on bitcoind and sanity checked."); + + Ok(bitcoind) +} + +// Create an Electrum interface from a client and BDK-based wallet, and do some sanity checks. +// If all went well, returns the interface to Electrum. +fn setup_electrum( + config: &Config, + db: sync::Arc>, +) -> Result { + let electrum_config = match config.bitcoin_backend.as_ref() { + Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config, + _ => Err(StartupError::MissingElectrumConfig)?, + }; + // First create the client to communicate with the Electrum server. + let client = electrum::client::Client::new(electrum_config) + .map_err(|e| StartupError::Electrum(ElectrumError::Client(e)))?; + // Then create the BDK-based wallet and populate it with DB data. + let mut db_conn = db.connection(); + let tip = db_conn.chain_tip(); + let coins: Vec<_> = db_conn + .coins(&[], &[]) + .into_values() + .map(|c| crate::bitcoin::Coin { + outpoint: c.outpoint, + amount: c.amount, + derivation_index: c.derivation_index, + is_change: c.is_change, + is_immature: c.is_immature, + block_info: c.block_info.map(|info| crate::bitcoin::BlockInfo { + height: info.height, + time: info.time, + }), + spend_txid: c.spend_txid, + spend_block: c.spend_block.map(|info| crate::bitcoin::BlockInfo { + height: info.height, + time: info.time, + }), + }) + .collect(); + let txids = db_conn.list_saved_txids(); + // This will only return those txs referenced by our coins, which may not be all of `txids`. + let txs: Vec<_> = db_conn + .list_wallet_transactions(&txids) + .into_iter() + .map(|(tx, _, _)| tx) + .collect(); + let (receive_index, change_index) = (db_conn.receive_index(), db_conn.change_index()); + let genesis_hash = { + let chain_hash = ChainHash::using_genesis_block(config.bitcoin_config.network); + BlockHash::from_byte_array(*chain_hash.as_bytes()) + }; + let bdk_wallet = electrum::wallet::BdkWallet::new( + &config.main_descriptor, + genesis_hash, + tip, + &coins, + &txs, + receive_index, + change_index, + ); + let full_scan = db_conn.rescan_timestamp().is_some(); + let electrum = Electrum::new(client, bdk_wallet, full_scan).map_err(StartupError::Electrum)?; + electrum + .sanity_checks(&genesis_hash) + .map_err(StartupError::Electrum)?; + Ok(electrum) +} + +#[derive(Clone)] +pub struct DaemonControl { + config: Config, + bitcoin: sync::Arc>, + poller_sender: mpsc::SyncSender, + // FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex? + db: sync::Arc>, + secp: secp256k1::Secp256k1, +} + +impl DaemonControl { + pub(crate) fn new( + config: Config, + bitcoin: sync::Arc>, + poller_sender: mpsc::SyncSender, + db: sync::Arc>, + secp: secp256k1::Secp256k1, + ) -> DaemonControl { + DaemonControl { + config, + bitcoin, + poller_sender, + db, + secp, + } + } + + // Useful for unit test to directly mess up with the DB + #[cfg(test)] + pub fn db(&self) -> sync::Arc> { + self.db.clone() + } +} + +/// The handle to a Liana daemon. It might either be the handle for a daemon which exposes a +/// JSONRPC server or one which exposes its API through a `DaemonControl`. +pub enum DaemonHandle { + Controller { + poller_sender: mpsc::SyncSender, + poller_handle: thread::JoinHandle<()>, + control: DaemonControl, + }, + Server { + poller_sender: mpsc::SyncSender, + poller_handle: thread::JoinHandle<()>, + rpcserver_shutdown: sync::Arc, + rpcserver_handle: thread::JoinHandle>, + }, +} + +impl DaemonHandle { + /// This starts the Liana daemon. A user of this interface should regularly poll the `is_alive` + /// method to check for internal errors. To shut down the daemon use the `stop` method. + /// + /// The `with_rpc_server` controls whether we should start a JSONRPC server to receive queries + /// or instead return a `DaemonControl` object for a caller to access the daemon's API. + /// + /// You may specify a custom Bitcoin interface through the `bitcoin` parameter. If `None`, the + /// default Bitcoin interface (`bitcoind` JSONRPC) will be used. + /// You may specify a custom Database interface through the `db` parameter. If `None`, the + /// default Database interface (SQLite) will be used. + pub fn start( + config: Config, + bitcoin: Option, + db: Option, + with_rpc_server: bool, + ) -> Result { + #[cfg(not(test))] + setup_panic_hook(); + + let secp = secp256k1::Secp256k1::verification_only(); + + // First, check the data directory + let mut data_dir = config + .data_dir() + .ok_or(StartupError::DefaultDataDirNotFound)?; + data_dir.push(config.bitcoin_config.network.to_string()); + let fresh_data_dir = !data_dir.as_path().exists(); + if fresh_data_dir { + create_datadir(&data_dir)?; + log::info!("Created a new data directory at '{}'", data_dir.display()); + } + + // Set up the connection to bitcoind (if using it) first as we may need it for the database + // migration when setting up SQLite below. + let bitcoind = if bitcoin.is_none() { + if let Some(config::BitcoinBackend::Bitcoind(_)) = &config.bitcoin_backend { + Some(setup_bitcoind(&config, &data_dir, fresh_data_dir)?) + } else { + None + } + } else { + None + }; + + // Then set up the database backend. + let db = match db { + Some(db) => sync::Arc::from(sync::Mutex::from(db)), + None => sync::Arc::from(sync::Mutex::from(setup_sqlite( + &config, + &data_dir, + fresh_data_dir, + &secp, + &bitcoind, + )?)) as sync::Arc>, + }; + + // Finally set up the Bitcoin backend. + let bit = match (bitcoin, &config.bitcoin_backend) { + (Some(bit), _) => sync::Arc::from(sync::Mutex::from(bit)), + (None, Some(config::BitcoinBackend::Bitcoind(..))) => sync::Arc::from( + sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")), + ) + as sync::Arc>, + (None, Some(config::BitcoinBackend::Electrum(..))) => { + sync::Arc::from(sync::Mutex::from(setup_electrum(&config, db.clone())?)) + } + (None, None) => Err(StartupError::MissingBitcoinBackendConfig)?, + }; + + // Start the poller thread. Keep the thread handle to be able to check if it crashed. Store + // an atomic to be able to stop it. + let mut bitcoin_poller = + poller::Poller::new(bit.clone(), db.clone(), config.main_descriptor.clone()); + let (poller_sender, poller_receiver) = mpsc::sync_channel(0); + let poller_handle = thread::Builder::new() + .name("Bitcoin Network poller".to_string()) + .spawn({ + let poll_interval = config.bitcoin_config.poll_interval_secs; + move || { + log::info!("Bitcoin poller started."); + bitcoin_poller.poll_forever(poll_interval, poller_receiver); + log::info!("Bitcoin poller stopped."); + } + }) + .expect("Spawning the poller thread must never fail."); + + // Create the API the external world will use to talk to us, either directly through the Rust + // structure or through the JSONRPC server we may setup below. + let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp); + + if with_rpc_server { + let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false)); + let rpcserver_handle = thread::Builder::new() + .name("Bitcoin Network poller".to_string()) + .spawn({ + let shutdown = rpcserver_shutdown.clone(); + move || { + let mut rpc_socket = data_dir; + rpc_socket.push("lianad_rpc"); + server::run(&rpc_socket, control, shutdown)?; + Ok(()) + } + }) + .expect("Spawning the RPC server thread should never fail."); + + return Ok(DaemonHandle::Server { + poller_sender, + poller_handle, + rpcserver_shutdown, + rpcserver_handle, + }); + } + + Ok(DaemonHandle::Controller { + poller_sender, + poller_handle, + control, + }) + } + + /// Start the Liana daemon with the default Bitcoin and database interfaces (`bitcoind` RPC + /// and SQLite). + pub fn start_default( + config: Config, + with_rpc_server: bool, + ) -> Result { + Self::start( + config, + Option::::None, + Option::::None, + with_rpc_server, + ) + } + + /// Check whether the daemon is still up and running. This needs to be regularly polled to + /// check for internal errors. If this returns `false`, collect the error using the `stop` + /// method. + pub fn is_alive(&self) -> bool { + match self { + Self::Controller { + ref poller_handle, .. + } => !poller_handle.is_finished(), + Self::Server { + ref poller_handle, + ref rpcserver_handle, + .. + } => !poller_handle.is_finished() && !rpcserver_handle.is_finished(), + } + } + + /// Stop the Liana daemon. This returns any error which may have occurred. + pub fn stop(self) -> Result<(), Box> { + match self { + Self::Controller { + poller_sender, + poller_handle, + .. + } => { + poller_sender + .send(poller::PollerMessage::Shutdown) + .expect("The other end should never have hung up before this."); + poller_handle.join().expect("Poller thread must not panic"); + Ok(()) + } + Self::Server { + poller_sender, + poller_handle, + rpcserver_shutdown, + rpcserver_handle, + } => { + poller_sender + .send(poller::PollerMessage::Shutdown) + .expect("The other end should never have hung up before this."); + rpcserver_shutdown.store(true, sync::atomic::Ordering::Relaxed); + rpcserver_handle + .join() + .expect("Poller thread must not panic")?; + poller_handle.join().expect("Poller thread must not panic"); + Ok(()) + } + } + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use crate::{ + config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth}, + testutils::*, + }; + + use liana::descriptors::LianaDescriptor; + + use miniscript::bitcoin; + use std::{ + fs, + io::{BufRead, BufReader, Write}, + net, path, + str::FromStr, + thread, time, + }; + + // Read all bytes from the socket until the end of a JSON object, good enough approximation. + fn read_til_json_end(stream: &mut net::TcpStream) { + stream + .set_read_timeout(Some(time::Duration::from_secs(5))) + .unwrap(); + let mut reader = BufReader::new(stream); + loop { + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + + if line.starts_with("Authorization") { + let mut buf = vec![0; 256]; + reader.read_until(b'}', &mut buf).unwrap(); + return; + } + } + } + + // Respond to the two "echo" sent at startup to sanity check the connection + fn complete_sanity_check(server: &net::TcpListener) { + let echo_resp = + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes(); + + // Read the first echo, respond to it + { + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(echo_resp).unwrap(); + stream.flush().unwrap(); + } + + // Read the second echo, respond to it + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(echo_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a pruned getblockchaininfo telling them we are at version 24.0 + fn complete_version_check(server: &net::TcpListener) { + let net_resp = + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":240000}}\n" + .as_bytes(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(net_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a pruned getblockchaininfo telling them we are on mainnet + fn complete_network_check(server: &net::TcpListener) { + let net_resp = + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"chain\":\"main\"}}\n" + .as_bytes(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(net_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them responses for the calls involved when creating a fresh wallet + fn complete_wallet_creation(server: &net::TcpListener) { + { + let net_resp = + ["HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes()] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + { + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n" + .as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[{\"success\":true}]}\n" + .as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a dummy result to loadwallet. + fn complete_wallet_loading(server: &net::TcpListener) { + { + let listwallets_resp = + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(listwallets_resp).unwrap(); + stream.flush().unwrap(); + } + + let loadwallet_resp = + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n" + .as_bytes(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(loadwallet_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a response to 'listwallets' with the watchonly wallet path + fn complete_wallet_check(server: &net::TcpListener, watchonly_wallet_path: &str) { + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"".as_bytes(), + watchonly_wallet_path.as_bytes(), + "\"]}\n".as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a response to 'listdescriptors' with the receive and change descriptors + fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) { + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(), + receive_desc.as_bytes(), + "\",\"timestamp\":0},".as_bytes(), + "{\"desc\":\"".as_bytes(), + change_desc.as_bytes(), + "\",\"timestamp\":1}]}}\n".as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + // Send them a response to 'getblockhash' with the genesis block hash + fn complete_tip_init(server: &net::TcpListener) { + let net_resp = [ + "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"}\n".as_bytes(), + ] + .concat(); + let (mut stream, _) = server.accept().unwrap(); + read_til_json_end(&mut stream); + stream.write_all(&net_resp).unwrap(); + stream.flush().unwrap(); + } + + // TODO: we could move the dummy bitcoind thread stuff to the bitcoind module to test the + // bitcoind interface, and use the DummyLiana from testutils to sanity check the startup. + // Note that startup as checked by this unit test is also tested in the functional test + // framework. + #[test] + fn daemon_startup() { + let tmp_dir = tmp_dir(); + fs::create_dir_all(&tmp_dir).unwrap(); + let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")] + .iter() + .collect(); + let wo_path: path::PathBuf = [ + data_dir.as_path(), + path::Path::new("bitcoin"), + path::Path::new("lianad_watchonly_wallet"), + ] + .iter() + .collect(); + let wo_path = wo_path.to_str().unwrap().to_string(); + + // Configure a dummy bitcoind + let network = bitcoin::Network::Bitcoin; + let cookie: path::PathBuf = [ + tmp_dir.as_path(), + path::Path::new(&format!( + "dummy_bitcoind_{:?}.cookie", + thread::current().id() + )), + ] + .iter() + .collect(); + fs::write(&cookie, [0; 32]).unwrap(); // Will overwrite should it exist already + let addr: net::SocketAddr = + net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into(); + let server = net::TcpListener::bind(addr).unwrap(); + let addr = server.local_addr().unwrap(); + let bitcoin_config = BitcoinConfig { + network, + poll_interval_secs: time::Duration::from_secs(2), + }; + let bitcoind_config = BitcoindConfig { + addr, + rpc_auth: BitcoindRpcAuth::CookieFile(cookie), + }; + + // Create a dummy config with this bitcoind + let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn"; + let desc = LianaDescriptor::from_str(desc_str).unwrap(); + let receive_desc = desc.receive_descriptor().clone(); + let change_desc = desc.change_descriptor().clone(); + let config = Config { + bitcoin_config, + bitcoin_backend: Some(config::BitcoinBackend::Bitcoind(bitcoind_config)), + data_dir: Some(data_dir), + log_level: log::LevelFilter::Debug, + main_descriptor: desc, + }; + + // Start the daemon in a new thread so the current one acts as the bitcoind server. + let t = thread::spawn({ + let config = config.clone(); + move || { + let handle = DaemonHandle::start_default(config, false).unwrap(); + handle.stop().unwrap(); + } + }); + complete_sanity_check(&server); + complete_version_check(&server); + complete_network_check(&server); + complete_wallet_creation(&server); + complete_wallet_loading(&server); + complete_wallet_check(&server, &wo_path); + complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string()); + complete_tip_init(&server); + // We don't have to complete the sync check as the poller checks whether it needs to stop + // before checking the bitcoind sync status. + t.join().unwrap(); + + // The datadir is created now, so if we restart it it won't create the wo wallet. + let t = thread::spawn({ + let config = config.clone(); + move || { + let handle = DaemonHandle::start_default(config, false).unwrap(); + handle.stop().unwrap(); + } + }); + complete_sanity_check(&server); + complete_version_check(&server); + complete_network_check(&server); + complete_wallet_loading(&server); + complete_wallet_check(&server, &wo_path); + complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string()); + // We don't have to complete the sync check as the poller checks whether it needs to stop + // before checking the bitcoind sync status. + t.join().unwrap(); + + fs::remove_dir_all(&tmp_dir).unwrap(); + } +} diff --git a/liana/src/testutils.rs b/lianad/src/testutils.rs similarity index 99% rename from liana/src/testutils.rs rename to lianad/src/testutils.rs index d7d7c6799..04ae68204 100644 --- a/liana/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -4,8 +4,9 @@ use crate::{ database::{ BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, }, - descriptors, DaemonControl, DaemonHandle, + DaemonControl, DaemonHandle, }; +use liana::descriptors; use std::convert::TryInto; use std::{ From da4c102a66c1f340797be13d70659b2f875c3438 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Tue, 19 Nov 2024 15:31:17 +0100 Subject: [PATCH 2/2] Fix reproducible system to handle lianad crate --- contrib/reproducible/docker/docker-build.sh | 4 ++++ contrib/reproducible/docker/macos_cmd.sh | 2 +- contrib/reproducible/guix/build.sh | 2 +- contrib/reproducible/guix/guix-build.sh | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contrib/reproducible/docker/docker-build.sh b/contrib/reproducible/docker/docker-build.sh index 2e42de529..55127d886 100755 --- a/contrib/reproducible/docker/docker-build.sh +++ b/contrib/reproducible/docker/docker-build.sh @@ -17,6 +17,8 @@ docker run --rm -ti \ -v "$PWD/Cargo.lock":/liana/Cargo.lock \ -v "$PWD/liana/Cargo.toml":/liana/liana/Cargo.toml \ -v "$PWD/liana/src":/liana/liana/src \ + -v "$PWD/lianad/Cargo.toml":/liana/lianad/Cargo.toml \ + -v "$PWD/lianad/src":/liana/lianad/src \ -v "$PWD/liana-gui/Cargo.toml":/liana/liana-gui/Cargo.toml \ -v "$PWD/liana-gui/src":/liana/liana-gui/src \ -v "$PWD/liana-ui/Cargo.toml":/liana/liana-ui/Cargo.toml \ @@ -41,6 +43,8 @@ docker run -ti \ -v "$PWD/Cargo.lock":/liana/Cargo.lock \ -v "$PWD/liana/Cargo.toml":/liana/liana/Cargo.toml \ -v "$PWD/liana/src":/liana/liana/src \ + -v "$PWD/lianad/Cargo.toml":/liana/lianad/Cargo.toml \ + -v "$PWD/lianad/src":/liana/lianad/src \ -v "$PWD/liana-gui/Cargo.toml":/liana/liana-gui/Cargo.toml \ -v "$PWD/liana-gui/src":/liana/liana-gui/src \ -v "$PWD/liana-ui/Cargo.toml":/liana/liana-ui/Cargo.toml \ diff --git a/contrib/reproducible/docker/macos_cmd.sh b/contrib/reproducible/docker/macos_cmd.sh index dd45f1498..332c70a23 100755 --- a/contrib/reproducible/docker/macos_cmd.sh +++ b/contrib/reproducible/docker/macos_cmd.sh @@ -29,7 +29,7 @@ cd .. # Finally build the projects using the toolchain just created. alias cargo="/liana/rust-1.71.1-x86_64-unknown-linux-gnu/cargo/bin/cargo" -for package_name in "liana" "liana-gui"; do +for package_name in "lianad" "liana-gui"; do PATH="$PATH:$PWD/osxcross/target/bin/" \ CC=o64-clang \ CXX=o64-clang++ \ diff --git a/contrib/reproducible/guix/build.sh b/contrib/reproducible/guix/build.sh index b0e736d79..5d50833da 100755 --- a/contrib/reproducible/guix/build.sh +++ b/contrib/reproducible/guix/build.sh @@ -25,7 +25,7 @@ export CARGO_HOME="/liana/.cargo" # We need to set RUSTC_BOOTSTRAP=1 as a workaround to be able to use unstable # features in the GUI dependencies -for package_name in "liana" "liana-gui"; do +for package_name in "lianad" "liana-gui"; do RUSTC_BOOTSTRAP=1 cargo -vvv \ --color always \ --frozen \ diff --git a/contrib/reproducible/guix/guix-build.sh b/contrib/reproducible/guix/guix-build.sh index ceeaaf214..6cdd380fa 100755 --- a/contrib/reproducible/guix/guix-build.sh +++ b/contrib/reproducible/guix/guix-build.sh @@ -80,6 +80,8 @@ time_machine shell --no-cwd \ --expose="$BUILD_ROOT/Cargo.lock=/liana/Cargo.lock" \ --expose="$PWD/liana/src=/liana/liana/src" \ --expose="$PWD/liana/Cargo.toml=/liana/liana/Cargo.toml" \ + --expose="$PWD/lianad/src=/liana/lianad/src" \ + --expose="$PWD/lianad/Cargo.toml=/liana/lianad/Cargo.toml" \ --expose="$PWD/liana-gui/Cargo.toml=/liana/liana-gui/Cargo.toml" \ --expose="$PWD/liana-gui/src=/liana/liana-gui/src" \ --expose="$PWD/liana-ui/src=/liana/liana-ui/src" \