Introduce the Bitcoin network interface along with a bitcoind module
This commit is contained in:
parent
8626e05a55
commit
c095346e17
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -43,6 +43,15 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64-compat"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.8.1"
|
||||
@ -76,6 +85,12 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
@ -170,6 +185,18 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpc"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f8423b78fc94d12ef1a4a9d13c348c9a78766dda0cc18817adf0faf77e670c8"
|
||||
dependencies = [
|
||||
"base64-compat",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
@ -209,6 +236,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"dirs",
|
||||
"fern",
|
||||
"jsonrpc",
|
||||
"log",
|
||||
"miniscript",
|
||||
"rusqlite",
|
||||
|
||||
@ -39,3 +39,6 @@ fern = "0.6"
|
||||
backtrace = "0.3"
|
||||
|
||||
rusqlite = { version = "0.28", features = ["bundled", "unlock_notify"] }
|
||||
|
||||
# To talk to bitcoind
|
||||
jsonrpc = "0.12"
|
||||
|
||||
491
src/bitcoin/d/mod.rs
Normal file
491
src/bitcoin/d/mod.rs
Normal file
@ -0,0 +1,491 @@
|
||||
///! Implementation of the Bitcoin interface using bitcoind.
|
||||
///!
|
||||
///! We use the RPC interface and a watchonly descriptor wallet.
|
||||
use crate::config;
|
||||
|
||||
use std::{fs, io, time::Duration};
|
||||
|
||||
use jsonrpc::{
|
||||
arg,
|
||||
client::Client,
|
||||
simple_http::{self, SimpleHttpTransport},
|
||||
};
|
||||
use miniscript::{bitcoin, Descriptor, DescriptorPublicKey};
|
||||
|
||||
use serde_json::Value as Json;
|
||||
|
||||
// If bitcoind takes more than 3 minutes to answer one of our queries, fail.
|
||||
const RPC_SOCKET_TIMEOUT: u64 = 180;
|
||||
|
||||
// Number of retries the client is allowed to do in case of timeout or i/o error
|
||||
// while communicating with the bitcoin daemon.
|
||||
// A retry happens every 1 second, this makes us give up after one minute.
|
||||
const BITCOIND_RETRY_LIMIT: usize = 60;
|
||||
|
||||
// The minimum bitcoind version that can be used with revaultd.
|
||||
const MIN_BITCOIND_VERSION: u64 = 239900;
|
||||
|
||||
/// An error in the bitcoind interface.
|
||||
#[derive(Debug)]
|
||||
pub enum BitcoindError {
|
||||
CookieFile(io::Error),
|
||||
/// Bitcoind server error.
|
||||
Server(jsonrpc::error::Error),
|
||||
/// They replied to a batch request omitting some responses.
|
||||
BatchMissingResponse,
|
||||
WalletCreation(String),
|
||||
DescriptorImport(String),
|
||||
WalletLoading(String),
|
||||
MissingOrTooManyWallet,
|
||||
InvalidVersion(u64),
|
||||
NetworkMismatch(String /*config*/, String /*bitcoind*/),
|
||||
MissingDescriptor,
|
||||
}
|
||||
|
||||
impl BitcoindError {
|
||||
/// Is bitcoind just starting ?
|
||||
pub fn is_warming_up(&self) -> bool {
|
||||
match self {
|
||||
// https://github.com/bitcoin/bitcoin/blob/dca80ffb45fcc8e6eedb6dc481d500dedab4248b/src/rpc/protocol.h#L49
|
||||
BitcoindError::Server(jsonrpc::error::Error::Rpc(jsonrpc::error::RpcError {
|
||||
code,
|
||||
..
|
||||
})) => *code == -28,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BitcoindError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
BitcoindError::CookieFile(e) => write!(f, "Reading bitcoind cookie file: {}", e),
|
||||
BitcoindError::Server(ref e) => write!(f, "Bitcoind RPC server error: {}", e),
|
||||
BitcoindError::BatchMissingResponse => write!(
|
||||
f,
|
||||
"Bitcoind server replied without enough responses to our batched request"
|
||||
),
|
||||
BitcoindError::WalletCreation(s) => write!(f, "Error creating watchonly wallet: {}", s),
|
||||
BitcoindError::DescriptorImport(s) => write!(
|
||||
f,
|
||||
"Error importing descriptor. Response from bitcoind: '{}'",
|
||||
s
|
||||
),
|
||||
BitcoindError::WalletLoading(s) => {
|
||||
write!(f, "Error when loading watchonly wallet: '{}'.", s)
|
||||
}
|
||||
BitcoindError::InvalidVersion(v) => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid bitcoind version '{}', minimum supported is '{}'.",
|
||||
v, MIN_BITCOIND_VERSION
|
||||
)
|
||||
}
|
||||
BitcoindError::NetworkMismatch(conf_net, bitcoind_net) => {
|
||||
write!(
|
||||
f,
|
||||
"Network mismatch. We are supposed to run on '{}' but bitcoind is on '{}'.",
|
||||
conf_net, bitcoind_net
|
||||
)
|
||||
}
|
||||
BitcoindError::MissingOrTooManyWallet => {
|
||||
write!(
|
||||
f,
|
||||
"No, or too many, watchonly wallet(s) loaded on bitcoind."
|
||||
)
|
||||
}
|
||||
BitcoindError::MissingDescriptor => {
|
||||
write!(f, "The watchonly wallet loaded on bitcoind does not have the main descriptor imported.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BitcoindError {}
|
||||
|
||||
impl From<jsonrpc::error::Error> for BitcoindError {
|
||||
fn from(e: jsonrpc::error::Error) -> Self {
|
||||
Self::Server(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<simple_http::Error> for BitcoindError {
|
||||
fn from(e: simple_http::Error) -> Self {
|
||||
jsonrpc::error::Error::Transport(Box::new(e)).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BitcoinD {
|
||||
node_client: Client,
|
||||
watchonly_client: Client,
|
||||
watchonly_wallet_path: String,
|
||||
/// How many times we'll retry upon failure to send a request.
|
||||
retries: usize,
|
||||
}
|
||||
|
||||
macro_rules! params {
|
||||
($($param:expr),* $(,)?) => {
|
||||
[
|
||||
$(
|
||||
arg($param),
|
||||
)*
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
impl BitcoinD {
|
||||
/// Create a new bitcoind interface. This tests the connection to bitcoind and disables retries
|
||||
/// on failure to send a request.
|
||||
pub fn new(
|
||||
config: &config::BitcoindConfig,
|
||||
watchonly_wallet_path: String,
|
||||
) -> Result<BitcoinD, BitcoindError> {
|
||||
let cookie_string =
|
||||
fs::read_to_string(&config.cookie_path).map_err(BitcoindError::CookieFile)?;
|
||||
|
||||
// Create a dummy client with a low timeout first to test the connection
|
||||
let dummy_node_client = Client::with_transport(
|
||||
SimpleHttpTransport::builder()
|
||||
.url(&config.addr.to_string())
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(3))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let req = dummy_node_client.build_request("echo", &[]);
|
||||
dummy_node_client.send_request(req.clone())?;
|
||||
|
||||
let node_client = Client::with_transport(
|
||||
SimpleHttpTransport::builder()
|
||||
.url(&config.addr.to_string())
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
|
||||
// Create a dummy client with a low timeout first to test the connection
|
||||
let url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path);
|
||||
let dummy_wo_client = Client::with_transport(
|
||||
SimpleHttpTransport::builder()
|
||||
.url(&url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(3))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let req = dummy_wo_client.build_request("echo", &[]);
|
||||
dummy_wo_client.send_request(req.clone())?;
|
||||
|
||||
let watchonly_url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path);
|
||||
let watchonly_client = Client::with_transport(
|
||||
SimpleHttpTransport::builder()
|
||||
.url(&watchonly_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
|
||||
Ok(BitcoinD {
|
||||
node_client,
|
||||
watchonly_client,
|
||||
watchonly_wallet_path,
|
||||
retries: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set how many times we'll retry a failed request. If passed None will set to default.
|
||||
pub fn with_retry_limit(mut self, retry_limit: Option<usize>) -> Self {
|
||||
self.retries = retry_limit.unwrap_or(BITCOIND_RETRY_LIMIT);
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrapper to retry a request sent to bitcoind upon IO failure
|
||||
/// according to the configured number of retries.
|
||||
fn retry<T, R: Fn() -> Result<T, BitcoindError>>(
|
||||
&self,
|
||||
request: R,
|
||||
) -> Result<T, BitcoindError> {
|
||||
let mut error: Option<BitcoindError> = None;
|
||||
for i in 0..self.retries + 1 {
|
||||
match request() {
|
||||
Ok(res) => return Ok(res),
|
||||
Err(e) => {
|
||||
if e.is_warming_up() {
|
||||
error = Some(e)
|
||||
} else if let BitcoindError::Server(jsonrpc::Error::Transport(ref err)) = e {
|
||||
match err.downcast_ref::<simple_http::Error>() {
|
||||
Some(simple_http::Error::Timeout)
|
||||
| Some(simple_http::Error::SocketError(_))
|
||||
| Some(simple_http::Error::HttpErrorCode(503)) => {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
log::debug!("Retrying RPC request to bitcoind: attempt #{}", i);
|
||||
error = Some(e);
|
||||
}
|
||||
_ => return Err(e),
|
||||
}
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(error.expect("Always set if we reach this point"))
|
||||
}
|
||||
|
||||
fn make_request<'a, 'b>(
|
||||
&self,
|
||||
client: &Client,
|
||||
method: &'a str,
|
||||
params: &'b [Box<serde_json::value::RawValue>],
|
||||
) -> Result<Json, BitcoindError> {
|
||||
self.retry(|| {
|
||||
let req = client.build_request(method, params);
|
||||
log::trace!("Sending to bitcoind: {:#?}", req);
|
||||
match client.send_request(req) {
|
||||
Ok(resp) => {
|
||||
let res = resp.result().map_err(BitcoindError::Server)?;
|
||||
log::trace!("Got from bitcoind: {:#?}", res);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
Err(e) => Err(BitcoindError::Server(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn make_node_request(&self, method: &str, params: &[Box<serde_json::value::RawValue>]) -> Json {
|
||||
self.make_request(&self.node_client, method, params)
|
||||
.expect("We must not fail to make a request for more than a minute")
|
||||
}
|
||||
|
||||
fn make_fallible_node_request(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &[Box<serde_json::value::RawValue>],
|
||||
) -> Result<Json, BitcoindError> {
|
||||
self.make_request(&self.node_client, method, params)
|
||||
}
|
||||
|
||||
fn make_wallet_request(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &[Box<serde_json::value::RawValue>],
|
||||
) -> Json {
|
||||
self.make_request(&self.watchonly_client, method, params)
|
||||
.expect("We must not fail to make a request for more than a minute")
|
||||
}
|
||||
|
||||
fn get_bitcoind_version(&self) -> u64 {
|
||||
self.make_node_request("getnetworkinfo", &[])
|
||||
.get("version")
|
||||
.map(Json::as_u64)
|
||||
.flatten()
|
||||
.expect("Missing or invalid 'version' in 'getnetworkinfo' result?")
|
||||
}
|
||||
|
||||
fn get_network_bip70(&self) -> String {
|
||||
self.make_node_request("getblockchaininfo", &[])
|
||||
.get("chain")
|
||||
.map(Json::as_str)
|
||||
.flatten()
|
||||
.expect("Missing or invalid 'chain' in 'getblockchaininfo' result?")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn list_wallets(&self) -> Vec<String> {
|
||||
self.make_node_request("listwallets", &[])
|
||||
.as_array()
|
||||
.expect("API break, 'listwallets' didn't return an array.")
|
||||
.iter()
|
||||
.map(|json_str| {
|
||||
json_str
|
||||
.as_str()
|
||||
.expect("API break: 'listwallets' contains a non-string value")
|
||||
.to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unload_wallet(&self, wallet_path: String) -> Option<String> {
|
||||
self.make_node_request("unloadwallet", ¶ms!(Json::String(wallet_path),))
|
||||
.get("warning")
|
||||
.expect("No 'warning' in 'unloadwallet' response?")
|
||||
.as_str()
|
||||
.and_then(|w| {
|
||||
if w.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(w.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_wallet(&self, wallet_path: String) -> Option<String> {
|
||||
let res = self.make_node_request(
|
||||
"createwallet",
|
||||
¶ms!(
|
||||
Json::String(wallet_path),
|
||||
Json::Bool(true), // watchonly
|
||||
Json::Bool(true), // blank
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(warning) = res.get("warning").map(Json::as_str).flatten() {
|
||||
return Some(warning.to_string());
|
||||
}
|
||||
if res.get("name").is_none() {
|
||||
return Some("Unknown error when create watchonly wallet".to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// TODO: rescan feature will probably need another timestamp than 'now'
|
||||
fn import_descriptor(&self, descriptor: &Descriptor<DescriptorPublicKey>) -> Option<String> {
|
||||
let descriptors = vec![serde_json::json!({
|
||||
"desc": descriptor.to_string(),
|
||||
"timestamp": "now",
|
||||
"active": false,
|
||||
})];
|
||||
|
||||
let res = self.make_wallet_request("importdescriptors", ¶ms!(Json::Array(descriptors)));
|
||||
let all_succeeded = res
|
||||
.as_array()
|
||||
.map(|results| {
|
||||
results.iter().all(|res| {
|
||||
res.get("success")
|
||||
.map(Json::as_bool)
|
||||
.flatten()
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if all_succeeded {
|
||||
None
|
||||
} else {
|
||||
Some(res.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn list_descriptors(&self) -> Vec<String> {
|
||||
self.make_wallet_request("listdescriptors", &[])
|
||||
.get("descriptors")
|
||||
.and_then(Json::as_array)
|
||||
.expect("Missing or invalid 'descriptors' field in 'listdescriptors' response")
|
||||
.iter()
|
||||
.map(|elem| {
|
||||
elem.get("desc")
|
||||
.and_then(Json::as_str)
|
||||
.expect(
|
||||
"Missing or invalid 'desc' field in 'listdescriptors' response's entries",
|
||||
)
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
/// Create the watchonly wallet on bitcoind, and import it the main descriptor.
|
||||
pub fn create_watchonly_wallet(
|
||||
&self,
|
||||
main_descriptor: &Descriptor<DescriptorPublicKey>,
|
||||
) -> Result<(), BitcoindError> {
|
||||
// Remove any leftover. This can happen if we delete the watchonly wallet but don't restart
|
||||
// bitcoind.
|
||||
while self.list_wallets().contains(&self.watchonly_wallet_path) {
|
||||
log::info!("Found a leftover watchonly wallet loaded on bitcoind. Removing it.");
|
||||
if let Some(e) = self.unload_wallet(self.watchonly_wallet_path.clone()) {
|
||||
log::error!(
|
||||
"Unloading wallet '{}': '{}'",
|
||||
&self.watchonly_wallet_path,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now create the wallet and import the main descriptor.
|
||||
if let Some(err) = self.create_wallet(self.watchonly_wallet_path.clone()) {
|
||||
return Err(BitcoindError::WalletCreation(err));
|
||||
}
|
||||
if let Some(err) = self.import_descriptor(main_descriptor) {
|
||||
return Err(BitcoindError::DescriptorImport(err));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn maybe_load_watchonly_wallet(&self) -> Result<(), BitcoindError> {
|
||||
match self.make_fallible_node_request(
|
||||
"loadwallet",
|
||||
¶ms!(Json::String(self.watchonly_wallet_path.clone()),),
|
||||
) {
|
||||
Err(e) => {
|
||||
if e.to_string().contains("is already loaded") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
Ok(res) => {
|
||||
if let Some(warning) = res.get("warning").map(Json::as_str).flatten() {
|
||||
Err(BitcoindError::WalletLoading(warning.to_string()))
|
||||
} else if res.get("name").is_none() {
|
||||
Err(BitcoindError::WalletLoading(res.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform various sanity checks on the bitcoind instance.
|
||||
pub fn sanity_check(
|
||||
&self,
|
||||
main_descriptor: &Descriptor<DescriptorPublicKey>,
|
||||
config_network: bitcoin::Network,
|
||||
) -> Result<(), BitcoindError> {
|
||||
// Check the minimum supported bitcoind version
|
||||
let version = self.get_bitcoind_version();
|
||||
if version < MIN_BITCOIND_VERSION {
|
||||
return Err(BitcoindError::InvalidVersion(version));
|
||||
}
|
||||
|
||||
// Check bitcoind is running on the right network
|
||||
let bitcoind_net = self.get_network_bip70();
|
||||
let bip70_net = match config_network {
|
||||
bitcoin::Network::Bitcoin => "main",
|
||||
bitcoin::Network::Testnet => "test",
|
||||
bitcoin::Network::Regtest => "regtest",
|
||||
bitcoin::Network::Signet => "signet",
|
||||
};
|
||||
if bitcoind_net != bip70_net {
|
||||
return Err(BitcoindError::NetworkMismatch(
|
||||
bip70_net.to_string(),
|
||||
bitcoind_net,
|
||||
));
|
||||
}
|
||||
|
||||
// Check our watchonly wallet is loaded
|
||||
if self
|
||||
.list_wallets()
|
||||
.iter()
|
||||
.filter(|s| s == &&self.watchonly_wallet_path)
|
||||
.count()
|
||||
!= 1
|
||||
{
|
||||
return Err(BitcoindError::MissingOrTooManyWallet);
|
||||
}
|
||||
|
||||
// Check our main descriptor is imported in this wallet.
|
||||
if !self
|
||||
.list_descriptors()
|
||||
.contains(&main_descriptor.to_string())
|
||||
{
|
||||
return Err(BitcoindError::MissingDescriptor);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
src/bitcoin/mod.rs
Normal file
6
src/bitcoin/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
///! Interface to the Bitcoin network.
|
||||
///!
|
||||
///! Broadcast transactions, poll for new unspent coins, gather fee estimates.
|
||||
pub mod d;
|
||||
|
||||
pub trait BitcoinInterface {}
|
||||
259
src/bitcoind/mod.rs
Normal file
259
src/bitcoind/mod.rs
Normal file
@ -0,0 +1,259 @@
|
||||
pub mod interface;
|
||||
pub mod poller;
|
||||
pub mod utils;
|
||||
|
||||
use crate::config::BitcoindConfig;
|
||||
use crate::{database::DatabaseError, revaultd::RevaultD, threadmessages::BitcoindMessageOut};
|
||||
use interface::{BitcoinD, WalletTransaction};
|
||||
use poller::poller_main;
|
||||
use revault_tx::bitcoin::{Network, Txid};
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::Receiver,
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use jsonrpc::{
|
||||
error::{Error, RpcError},
|
||||
simple_http,
|
||||
};
|
||||
|
||||
/// Number of retries the client is allowed to do in case of timeout or i/o error
|
||||
/// while communicating with the bitcoin daemon.
|
||||
/// A retry happens every 1 second, this makes us give up after one minute.
|
||||
const BITCOIND_RETRY_LIMIT: usize = 60;
|
||||
|
||||
/// The minimum bitcoind version that can be used with revaultd.
|
||||
const MIN_BITCOIND_VERSION: u64 = 220000;
|
||||
|
||||
/// An error happened in the bitcoind-manager thread
|
||||
#[derive(Debug)]
|
||||
pub enum BitcoindError {
|
||||
/// It can be related to us..
|
||||
Custom(String),
|
||||
/// Or directly to bitcoind's RPC server
|
||||
Server(Error),
|
||||
/// They replied to a batch request omitting some responses
|
||||
BatchMissingResponse,
|
||||
RevaultTx(revault_tx::Error),
|
||||
}
|
||||
|
||||
impl BitcoindError {
|
||||
/// Is bitcoind just starting ?
|
||||
pub fn is_warming_up(&self) -> bool {
|
||||
match self {
|
||||
// https://github.com/bitcoin/bitcoin/blob/dca80ffb45fcc8e6eedb6dc481d500dedab4248b/src/rpc/protocol.h#L49
|
||||
BitcoindError::Server(Error::Rpc(RpcError { code, .. })) => *code == -28,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BitcoindError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
BitcoindError::Custom(ref s) => write!(f, "Bitcoind manager error: {}", s),
|
||||
BitcoindError::Server(ref e) => write!(f, "Bitcoind server error: {}", e),
|
||||
BitcoindError::BatchMissingResponse => write!(
|
||||
f,
|
||||
"Bitcoind server replied without enough responses to our batched request"
|
||||
),
|
||||
BitcoindError::RevaultTx(ref s) => write!(f, "Bitcoind manager error: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BitcoindError {}
|
||||
|
||||
// FIXME: remove this (and probably the 'Custom' variant too. If we fail to access the DB we should
|
||||
// panic.
|
||||
impl From<DatabaseError> for BitcoindError {
|
||||
fn from(e: DatabaseError) -> Self {
|
||||
Self::Custom(format!("Database error in bitcoind thread: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<simple_http::Error> for BitcoindError {
|
||||
fn from(e: simple_http::Error) -> Self {
|
||||
Self::Server(Error::Transport(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<revault_tx::Error> for BitcoindError {
|
||||
fn from(e: revault_tx::Error) -> Self {
|
||||
Self::RevaultTx(e)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bitcoind_network(
|
||||
bitcoind: &BitcoinD,
|
||||
config_network: &Network,
|
||||
) -> Result<(), BitcoindError> {
|
||||
let chaininfo = bitcoind.getblockchaininfo()?;
|
||||
let chain = chaininfo
|
||||
.get("chain")
|
||||
.and_then(|c| c.as_str())
|
||||
.ok_or_else(|| {
|
||||
BitcoindError::Custom("No valid 'chain' in getblockchaininfo response?".to_owned())
|
||||
})?;
|
||||
let bip70_net = match config_network {
|
||||
Network::Bitcoin => "main",
|
||||
Network::Testnet => "test",
|
||||
Network::Regtest => "regtest",
|
||||
Network::Signet => "signet",
|
||||
};
|
||||
|
||||
if !bip70_net.eq(chain) {
|
||||
return Err(BitcoindError::Custom(format!(
|
||||
"Wrong network, bitcoind is on '{}' but our config says '{}' ({})",
|
||||
chain, bip70_net, config_network
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_bitcoind_version(bitcoind: &BitcoinD) -> Result<(), BitcoindError> {
|
||||
let network_info = bitcoind.getnetworkinfo()?;
|
||||
let bitcoind_version = network_info
|
||||
.get("version")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| {
|
||||
BitcoindError::Custom("No valid 'version' in getnetworkinfo response?".to_owned())
|
||||
})?;
|
||||
|
||||
if bitcoind_version < MIN_BITCOIND_VERSION {
|
||||
return Err(BitcoindError::Custom(format!(
|
||||
"Revaultd needs bitcoind v{} or greater to operate but v{} was found",
|
||||
MIN_BITCOIND_VERSION, bitcoind_version
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Some sanity checks to be done at startup to make sure our bitcoind isn't going to fail under
|
||||
/// our feet for a legitimate reason.
|
||||
fn bitcoind_sanity_checks(
|
||||
bitcoind: &BitcoinD,
|
||||
bitcoind_config: &BitcoindConfig,
|
||||
) -> Result<(), BitcoindError> {
|
||||
check_bitcoind_version(bitcoind)?;
|
||||
check_bitcoind_network(bitcoind, &bitcoind_config.network)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Connects to and sanity checks bitcoind.
|
||||
pub fn start_bitcoind(revaultd: &mut RevaultD) -> Result<BitcoinD, BitcoindError> {
|
||||
let bitcoind = BitcoinD::new(
|
||||
&revaultd.bitcoind_config,
|
||||
revaultd
|
||||
.watchonly_wallet_file()
|
||||
.expect("Wallet id is set at startup in setup_db()"),
|
||||
revaultd
|
||||
.cpfp_wallet_file()
|
||||
.expect("Wallet id is set at startup in setup_db()"),
|
||||
)
|
||||
.map_err(|e| BitcoindError::Custom(format!("Could not connect to bitcoind: {}", e)))?;
|
||||
|
||||
while let Err(e) = bitcoind_sanity_checks(&bitcoind, &revaultd.bitcoind_config) {
|
||||
if e.is_warming_up() {
|
||||
log::info!("Bitcoind is warming up. Waiting for it to be back up.");
|
||||
thread::sleep(Duration::from_secs(3))
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bitcoind.with_retry_limit(BITCOIND_RETRY_LIMIT))
|
||||
}
|
||||
|
||||
fn wallet_transaction(bitcoind: &BitcoinD, txid: Txid) -> Option<WalletTransaction> {
|
||||
bitcoind
|
||||
.get_wallet_transaction(&txid)
|
||||
.map_err(|res| {
|
||||
log::trace!(
|
||||
"Got '{:?}' from bitcoind when requesting wallet transaction '{}'",
|
||||
res,
|
||||
txid
|
||||
);
|
||||
res
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// The bitcoind event loop.
|
||||
/// Listens for bitcoind requests (wallet / chain) and poll bitcoind every 30 seconds,
|
||||
/// updating our state accordingly.
|
||||
pub fn bitcoind_main_loop(
|
||||
rx: Receiver<BitcoindMessageOut>,
|
||||
revaultd: Arc<RwLock<RevaultD>>,
|
||||
bitcoind: BitcoinD,
|
||||
) -> Result<(), BitcoindError> {
|
||||
let bitcoind = Arc::new(RwLock::new(bitcoind));
|
||||
// The verification progress announced by bitcoind *at startup* thus won't be updated
|
||||
// after startup check. Should be *exactly* 1.0 when synced, but hey, floats so we are
|
||||
// careful.
|
||||
let sync_progress = Arc::new(RwLock::new(0.0f64));
|
||||
// Used to shutdown the poller thread
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// We use a thread to 1) wait for bitcoind to be synced 2) poll listunspent
|
||||
let poller_thread = std::thread::spawn({
|
||||
let _bitcoind = bitcoind.clone();
|
||||
let _sync_progress = sync_progress.clone();
|
||||
let _shutdown = shutdown.clone();
|
||||
move || poller_main(revaultd, _bitcoind, _sync_progress, _shutdown)
|
||||
});
|
||||
|
||||
for msg in rx {
|
||||
match msg {
|
||||
BitcoindMessageOut::Shutdown => {
|
||||
log::info!("Bitcoind received shutdown from main. Exiting.");
|
||||
shutdown.store(true, Ordering::Relaxed);
|
||||
poller_thread
|
||||
.join()
|
||||
.expect("Joining bitcoind poller thread");
|
||||
return Ok(());
|
||||
}
|
||||
BitcoindMessageOut::SyncProgress(resp_tx) => {
|
||||
resp_tx.send(*sync_progress.read().unwrap()).map_err(|e| {
|
||||
BitcoindError::Custom(format!(
|
||||
"Sending synchronization progress to main thread: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
BitcoindMessageOut::WalletTransaction(txid, resp_tx) => {
|
||||
log::trace!("Received 'wallettransaction' from main thread");
|
||||
// FIXME: what if bitcoind isn't synced?
|
||||
resp_tx
|
||||
.send(wallet_transaction(&bitcoind.read().unwrap(), txid))
|
||||
.map_err(|e| {
|
||||
BitcoindError::Custom(format!(
|
||||
"Sending wallet transaction to main thread: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
BitcoindMessageOut::BroadcastTransactions(txs, resp_tx) => {
|
||||
log::trace!("Received 'broadcastransactions' from main thread");
|
||||
resp_tx
|
||||
.send(bitcoind.read().unwrap().broadcast_transactions(&txs))
|
||||
.map_err(|e| {
|
||||
BitcoindError::Custom(format!(
|
||||
"Sending transactions broadcast result to main thread: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
242
src/lib.rs
242
src/lib.rs
@ -1,7 +1,9 @@
|
||||
mod bitcoin;
|
||||
pub mod config;
|
||||
mod database;
|
||||
|
||||
use crate::{
|
||||
bitcoin::d::{BitcoinD, BitcoindError},
|
||||
config::{config_folder_path, Config},
|
||||
database::sqlite::{FreshDbOptions, SqliteDb, SqliteDbError},
|
||||
};
|
||||
@ -47,6 +49,7 @@ pub enum StartupError {
|
||||
DefaultDataDirNotFound,
|
||||
DatadirCreation(path::PathBuf, io::Error),
|
||||
Database(SqliteDbError),
|
||||
Bitcoind(BitcoindError),
|
||||
}
|
||||
|
||||
impl fmt::Display for StartupError {
|
||||
@ -61,7 +64,8 @@ impl fmt::Display for StartupError {
|
||||
f,
|
||||
"Could not create data directory at '{}': '{}'", dir_path.display(), e
|
||||
),
|
||||
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e)
|
||||
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
|
||||
Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +84,12 @@ impl From<SqliteDbError> for StartupError {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -124,6 +134,7 @@ impl DaemonHandle {
|
||||
log::info!("Created a new data directory at '{}'", data_dir.display());
|
||||
}
|
||||
|
||||
// Then set up the database
|
||||
let db_path: path::PathBuf = [data_dir.as_path(), path::Path::new("minisafed.sqlite3")]
|
||||
.iter()
|
||||
.collect();
|
||||
@ -137,6 +148,27 @@ impl DaemonHandle {
|
||||
};
|
||||
let db = SqliteDb::new(db_path, options)?;
|
||||
db.sanity_check(config.bitcoind_config.network, &config.main_descriptor)?;
|
||||
log::info!("Database initialized and checked.");
|
||||
|
||||
// Now set up the bitcoind interface
|
||||
let wo_path: path::PathBuf = [
|
||||
data_dir.as_path(),
|
||||
path::Path::new("minisafed_watchonly_wallet"),
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
let bitcoind = BitcoinD::new(
|
||||
&config.bitcoind_config,
|
||||
wo_path.to_str().expect("Must be valid unicode").to_string(),
|
||||
)?;
|
||||
if fresh_data_dir {
|
||||
bitcoind.create_watchonly_wallet(&config.main_descriptor)?;
|
||||
log::info!("Created a new watchonly wallet on bitcoind.");
|
||||
}
|
||||
bitcoind.maybe_load_watchonly_wallet()?;
|
||||
bitcoind.sanity_check(&config.main_descriptor, config.bitcoind_config.network)?;
|
||||
bitcoind.with_retry_limit(None);
|
||||
log::info!("Connection to bitcoind established and checked.");
|
||||
|
||||
Ok(Self {})
|
||||
}
|
||||
@ -152,7 +184,141 @@ mod tests {
|
||||
use crate::config::BitcoindConfig;
|
||||
|
||||
use miniscript::{bitcoin, Descriptor, DescriptorPublicKey};
|
||||
use std::{env, fs, net, path, process, str::FromStr, thread, time};
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net, path, process,
|
||||
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 23.99
|
||||
fn complete_version_check(server: &net::TcpListener) {
|
||||
let net_resp =
|
||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":239900}}\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 net_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(net_resp).unwrap();
|
||||
stream.flush().unwrap();
|
||||
}
|
||||
|
||||
// Send them a response to 'listwallets' with the watchonly wallet path
|
||||
fn complete_wallet_check<'a>(server: &net::TcpListener, watchonly_wallet_path: &'a 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 main descriptor
|
||||
fn complete_desc_check<'a>(server: &net::TcpListener, desc: &'a str) {
|
||||
let net_resp = [
|
||||
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
|
||||
desc.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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_startup() {
|
||||
@ -165,24 +331,78 @@ mod tests {
|
||||
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("minisafed_watchonly_wallet"),
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
let wo_path = wo_path.to_str().unwrap().to_string();
|
||||
|
||||
let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))";
|
||||
// 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 bitcoind_config = BitcoindConfig {
|
||||
network,
|
||||
addr,
|
||||
cookie_path: cookie.clone(),
|
||||
poll_interval_secs: time::Duration::from_secs(2),
|
||||
};
|
||||
|
||||
// Create a dummy config with this bitcoind
|
||||
let desc_str = "wsh(andor(pk(03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a),older(10000),pk(0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce)))#39x77spy";
|
||||
let desc = Descriptor::<DescriptorPublicKey>::from_str(desc_str).unwrap();
|
||||
let config = Config {
|
||||
bitcoind_config: BitcoindConfig {
|
||||
network: bitcoin::Network::Bitcoin,
|
||||
cookie_path: path::PathBuf::new(),
|
||||
addr: net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::LOCALHOST), 0),
|
||||
poll_interval_secs: time::Duration::from_secs(1),
|
||||
},
|
||||
bitcoind_config,
|
||||
data_dir: Some(data_dir.clone()),
|
||||
daemon: None,
|
||||
log_level: log::LevelFilter::Debug,
|
||||
main_descriptor: desc,
|
||||
};
|
||||
|
||||
let handle = DaemonHandle::start(config).unwrap();
|
||||
handle.shutdown();
|
||||
// Start the daemon in a new thread so the current one acts as the bitcoind server.
|
||||
let daemon_thread = thread::spawn({
|
||||
let config = config.clone();
|
||||
move || {
|
||||
let handle = DaemonHandle::start(config).unwrap();
|
||||
handle.shutdown();
|
||||
}
|
||||
});
|
||||
complete_sanity_check(&server);
|
||||
complete_wallet_creation(&server);
|
||||
complete_wallet_loading(&server);
|
||||
complete_version_check(&server);
|
||||
complete_network_check(&server);
|
||||
complete_wallet_check(&server, &wo_path);
|
||||
complete_desc_check(&server, desc_str);
|
||||
daemon_thread.join().unwrap();
|
||||
|
||||
// The datadir is created now, so if we restart it it won't create the wo wallet.
|
||||
let daemon_thread = thread::spawn(move || {
|
||||
let handle = DaemonHandle::start(config).unwrap();
|
||||
handle.shutdown();
|
||||
});
|
||||
complete_sanity_check(&server);
|
||||
complete_wallet_loading(&server);
|
||||
complete_version_check(&server);
|
||||
complete_network_check(&server);
|
||||
complete_wallet_check(&server, &wo_path);
|
||||
complete_desc_check(&server, desc_str);
|
||||
daemon_thread.join().unwrap();
|
||||
|
||||
fs::remove_dir_all(&tmp_dir).unwrap();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user