Merge #1476: Split liana into liana and lianad crates

da4c102a66c1f340797be13d70659b2f875c3438 Fix reproducible system to handle lianad crate (edouardparis)
74820d97e9257e44c5c9d3bc64926e30066b285a Split into lianad and liana crate (edouardparis)

Pull request description:

  This change introduce two crates:
  - `lianad` the crate responsible to create the `lianad` and `liana-cli` binaries and expose daemon modules for `liana-gui` to embed
  - `liana` the crate responsible for the descriptors and the core logic of the bitcoin scripts and spend transactions creation. This will allow multiple platforms and mediums to start manipulating these modules without importing all the daemon dependencies (like the database or bitcoin core clients).

ACKs for top commit:
  jp1ac4:
    ACK da4c102a66.

Tree-SHA512: 85a268b2bffe5fe7b54d7b1e452dd2097ae606b903e5684a7b0e996b61981ab222b6718fb19739a05d1153b4d48a85d0ec3e1e15f68afe17936eec80111ecf04
This commit is contained in:
edouardparis 2024-11-19 19:02:49 +01:00
commit 1f7c41ae5b
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
64 changed files with 1077 additions and 1030 deletions

View File

@ -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

27
Cargo.lock generated
View File

@ -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"

View File

@ -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"}

View File

@ -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 \

View File

@ -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++ \

View File

@ -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 \

View File

@ -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" \

View File

@ -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"

View File

@ -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},

View File

@ -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},

View File

@ -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;

View File

@ -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::{

View File

@ -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,

View File

@ -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::{

View File

@ -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::{

View File

@ -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;

View File

@ -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::{

View File

@ -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};

View File

@ -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;

View File

@ -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<bool>,
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<bool>,
can_edit: bool,

View File

@ -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};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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;

View File

@ -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 {

View File

@ -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<Mutex<Signer>>,
) -> Result<PathBuf, Error> {
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

View File

@ -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,
}));

View File

@ -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;

View File

@ -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<State,
};
if let Some(daemon_config_path) = cfg.daemon_config_path {
liana::config::Config::from_file(Some(daemon_config_path.clone())).map_err(|e| match e {
lianad::config::Config::from_file(Some(daemon_config_path.clone())).map_err(|e| match e {
ConfigError::FileNotFound
| ConfigError::DatadirNotFound => {
format!(

View File

@ -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;

View File

@ -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,

View File

@ -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 {

View File

@ -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},

View File

@ -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();

View File

@ -1,4 +1,4 @@
use liana::config::BitcoinBackend;
use lianad::config::BitcoinBackend;
pub mod bitcoind;
pub mod electrum;

View File

@ -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"

View File

@ -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::<String>().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<io::Error> for StartupError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<SqliteDbError> for StartupError {
fn from(e: SqliteDbError) -> Self {
Self::Database(e)
}
}
impl From<BitcoindError> 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<secp256k1::VerifyOnly>,
bitcoind: &Option<BitcoinD>,
) -> Result<SqliteDb, StartupError> {
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::<collections::HashSet<_>>();
coins_txids
.into_iter()
.map(|txid| bit.get_transaction(&txid).map(|res| res.tx))
.collect::<Option<Vec<_>>>()
.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<BitcoinD, StartupError> {
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<sync::Mutex<dyn DatabaseInterface>>,
) -> Result<Electrum, StartupError> {
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<sync::Mutex<dyn BitcoinInterface>>,
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
// FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex?
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
}
impl DaemonControl {
pub(crate) fn new(
config: Config,
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> 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<sync::Mutex<dyn DatabaseInterface>> {
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::PollerMessage>,
poller_handle: thread::JoinHandle<()>,
control: DaemonControl,
},
Server {
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
poller_handle: thread::JoinHandle<()>,
rpcserver_shutdown: sync::Arc<sync::atomic::AtomicBool>,
rpcserver_handle: thread::JoinHandle<Result<(), io::Error>>,
},
}
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<impl BitcoinInterface + 'static>,
db: Option<impl DatabaseInterface + 'static>,
with_rpc_server: bool,
) -> Result<Self, StartupError> {
#[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<sync::Mutex<dyn DatabaseInterface>>,
};
// 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<sync::Mutex<dyn BitcoinInterface>>,
(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<DaemonHandle, StartupError> {
Self::start(
config,
Option::<BitcoinD>::None,
Option::<SqliteDb>::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<dyn error::Error>> {
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();
}
}

View File

@ -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.

54
lianad/Cargo.toml Normal file
View File

@ -0,0 +1,54 @@
[package]
name = "lianad"
version = "8.0.0"
authors = ["Antoine Poinsot <darosior@protonmail.com>"]
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 }

View File

@ -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,

View File

@ -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);

View File

@ -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::{

View File

@ -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;

View File

@ -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};

View File

@ -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)]

View File

@ -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},

View File

@ -7,18 +7,21 @@ mod utils;
use crate::{
bitcoin::BitcoinInterface,
database::{Coin, DatabaseConnection, DatabaseInterface},
descriptors,
miniscript::bitcoin::absolute::LockTime,
poller::PollerMessage,
spend::{
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
},
DaemonControl, VERSION,
};
pub use crate::database::{CoinStatus, LabelItem};
use liana::{
descriptors,
spend::{
self, create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes,
SpendCreationError, SpendOutputAddress, SpendTxFees, TxGetter,
},
};
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},

View File

@ -1,4 +1,4 @@
use crate::descriptors::LianaDescriptor;
use liana::descriptors::LianaDescriptor;
use std::{fmt, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};

View File

@ -25,8 +25,8 @@ use crate::{
},
Coin, CoinStatus, LabelItem,
},
descriptors::LianaDescriptor,
};
use liana::descriptors::LianaDescriptor;
use std::{
cmp,

View File

@ -1,4 +1,4 @@
use crate::descriptors::LianaDescriptor;
use liana::descriptors::LianaDescriptor;
use std::{convert::TryFrom, str::FromStr};

870
lianad/src/lib.rs Normal file
View File

@ -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::<String>().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<io::Error> for StartupError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<SqliteDbError> for StartupError {
fn from(e: SqliteDbError) -> Self {
Self::Database(e)
}
}
impl From<BitcoindError> 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<secp256k1::VerifyOnly>,
bitcoind: &Option<BitcoinD>,
) -> Result<SqliteDb, StartupError> {
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::<collections::HashSet<_>>();
coins_txids
.into_iter()
.map(|txid| bit.get_transaction(&txid).map(|res| res.tx))
.collect::<Option<Vec<_>>>()
.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<BitcoinD, StartupError> {
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<sync::Mutex<dyn DatabaseInterface>>,
) -> Result<Electrum, StartupError> {
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<sync::Mutex<dyn BitcoinInterface>>,
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
// FIXME: Should we require Sync on DatabaseInterface rather than using a Mutex?
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
}
impl DaemonControl {
pub(crate) fn new(
config: Config,
bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> 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<sync::Mutex<dyn DatabaseInterface>> {
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::PollerMessage>,
poller_handle: thread::JoinHandle<()>,
control: DaemonControl,
},
Server {
poller_sender: mpsc::SyncSender<poller::PollerMessage>,
poller_handle: thread::JoinHandle<()>,
rpcserver_shutdown: sync::Arc<sync::atomic::AtomicBool>,
rpcserver_handle: thread::JoinHandle<Result<(), io::Error>>,
},
}
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<impl BitcoinInterface + 'static>,
db: Option<impl DatabaseInterface + 'static>,
with_rpc_server: bool,
) -> Result<Self, StartupError> {
#[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<sync::Mutex<dyn DatabaseInterface>>,
};
// 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<sync::Mutex<dyn BitcoinInterface>>,
(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<DaemonHandle, StartupError> {
Self::start(
config,
Option::<BitcoinD>::None,
Option::<SqliteDb>::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<dyn error::Error>> {
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();
}
}

View File

@ -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::{