Skip to content

Commit

Permalink
Merge pull request fedimint#3777 from douglaz/recoverytool-tests
Browse files Browse the repository at this point in the history
feat: added recoverytool tests
  • Loading branch information
elsirion authored Nov 29, 2023
2 parents 6b83ef2 + f08256f commit 361a16f
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 14 deletions.
6 changes: 3 additions & 3 deletions devimint/src/federation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,14 @@ impl Federation {
cmd!("fedimint-cli", "--data-dir={cfg_dir}")
}

pub async fn pegin(&self, amount: u64) -> Result<()> {
info!(amount, "Peg-in");
pub async fn pegin(&self, amount_sats: u64) -> Result<()> {
info!(amount_sats, "Peg-in");
let deposit = cmd!(self, "deposit-address").out_json().await?;
let deposit_address = deposit["address"].as_str().unwrap();
let deposit_operation_id = deposit["operation_id"].as_str().unwrap();

self.bitcoind
.send_to(deposit_address.to_owned(), amount)
.send_to(deposit_address.to_owned(), amount_sats)
.await?;
self.bitcoind.mine_blocks(21).await?;

Expand Down
125 changes: 124 additions & 1 deletion devimint/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::collections::HashSet;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use std::{env, ffi};

use anyhow::{anyhow, bail, Context, Result};
use bitcoincore_rpc::bitcoin;
use bitcoincore_rpc::bitcoin::hashes::hex::ToHex;
use bitcoincore_rpc::bitcoin::Txid;
use bitcoincore_rpc::{bitcoin, RpcApi};
use clap::{Parser, Subcommand};
use cln_rpc::primitives::{Amount as ClnRpcAmount, AmountOrAny};
use devimint::federation::{Federation, Fedimintd};
Expand All @@ -20,6 +21,7 @@ use fedimint_core::task::{timeout, TaskGroup};
use fedimint_core::util::write_overwrite_async;
use fedimint_logging::LOG_DEVIMINT;
use ln_gateway::rpc::GatewayInfo;
use serde_json::json;
use tokio::fs;
use tokio::net::TcpStream;
use tracing::{debug, error, info, warn};
Expand Down Expand Up @@ -1380,6 +1382,120 @@ async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result
Ok(())
}

// TODO: test the direct method using tweaks
pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
#[allow(unused_variables)]
let DevFed {
bitcoind,
cln,
lnd,
fed,
gw_cln,
gw_lnd,
electrs,
esplora,
} = dev_fed;
let data_dir = env::var("FM_DATA_DIR")?;

let peg_in_values = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
for peg_in_value in &peg_in_values {
fed.pegin(*peg_in_value).await?;
}
let total_pegged_in = peg_in_values.iter().sum::<u64>();

let now = fedimint_core::time::now();
info!("Recovering using utxos method");
let output = cmd!(
"recoverytool",
"--readonly",
"--cfg",
"{data_dir}/server-0",
"utxos",
"--db",
"{data_dir}/server-0/database"
)
.env("FM_PASSWORD", "pass")
.out_json()
.await?;
let outputs = output.as_array().context("expected an array")?;
assert_eq!(outputs.len(), peg_in_values.len());

assert_eq!(
outputs
.iter()
.map(|o| o["amount_sat"].as_u64().unwrap())
.collect::<HashSet<_>>(),
peg_in_values
);
let utxos_descriptors = outputs
.iter()
.map(|o| o["descriptor"].as_str().unwrap())
.collect::<HashSet<_>>();

let descriptors_json = [serde_json::value::to_raw_value(&serde_json::Value::Array(
utxos_descriptors
.iter()
.map(|d| {
let object = json!({
"desc": d,
"timestamp": 0,
});
object
})
.collect::<Vec<_>>(),
))?];
info!("Getting wallet balances before import");
let balances_before = bitcoind.client().get_balances()?;
info!("Importing descriptors into bitcoin wallet");
let request = bitcoind
.client()
.get_jsonrpc_client()
.build_request("importdescriptors", &descriptors_json);
let response = bitcoind
.client()
.get_jsonrpc_client()
.send_request(request)?;
response.check_error()?;
info!("Getting wallet balances after import");
let balances_after = bitcoind.client().get_balances()?;
let diff = balances_after.mine.immature + balances_after.mine.trusted
- balances_before.mine.immature
- balances_before.mine.trusted;
// Funds from descriptors should match the total pegged in
assert_eq!(diff.to_sat(), total_pegged_in);
info!("Recovering using epochs method");
let outputs = loop {
let output = cmd!(
"recoverytool",
"--readonly",
"--cfg",
"{data_dir}/server-0",
"epochs",
"--db",
"{data_dir}/server-0/database"
)
.env("FM_PASSWORD", "pass")
.out_json()
.await?;
let outputs = output.as_array().context("expected an array")?;
if outputs.len() == peg_in_values.len() {
break outputs.clone();
} else if now.elapsed()? > Duration::from_secs(180) {
// 3 minutes should be enough to finish one or two sessions
bail!("recoverytool epochs method timed out");
} else {
fedimint_core::task::sleep(Duration::from_secs(1)).await
}
};
let epochs_descriptors = outputs
.iter()
.map(|o| o["descriptor"].as_str().unwrap())
.collect::<HashSet<_>>();
// Both methods should yield the same descriptors
assert_eq!(utxos_descriptors, epochs_descriptors);
Ok(())
}

#[derive(Subcommand)]
enum Cmd {
/// Spins up bitcoind, cln, lnd, electrs, esplora, and opens a channel
Expand Down Expand Up @@ -1420,6 +1536,8 @@ enum Cmd {
/// `devfed` then reboot gateway daemon for both CLN and LND. Test
/// afterward.
GatewayRebootTest,
/// `devfed` then tests if the recovery tool is able to do a basic recovery
RecoverytoolTests,
/// Rpc commands to the long running devimint instance. Could be entry point
/// for devimint as a cli
#[clap(flatten)]
Expand Down Expand Up @@ -1716,6 +1834,11 @@ async fn handle_command() -> Result<()> {
let dev_fed = dev_fed(&process_mgr).await?;
gw_reboot_test(dev_fed, &process_mgr).await?;
}
Cmd::RecoverytoolTests => {
let (process_mgr, _) = setup(args.common).await?;
let dev_fed = dev_fed(&process_mgr).await?;
recoverytool_test(dev_fed).await?;
}
Cmd::Rpc(rpc) => rpc_command(rpc, args.common).await?,
}
Ok(())
Expand Down
5 changes: 3 additions & 2 deletions recoverytool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ e-cash notes or other operational difficulties there are funds left in the feder
```
Tool to recover the on-chain wallet of a Fedimint federation
Usage: recoverytool [OPTIONS] <--cfg <CONFIG>|--descriptor <DESCRIPTOR>> <COMMAND>
Usage: recoverytool [OPTIONS] --password <PASSWORD> <--cfg <CONFIG>|--descriptor <DESCRIPTOR>> <COMMAND>
Commands:
direct Derive the wallet descriptor using a single tweak
Expand All @@ -24,10 +24,11 @@ Commands:
Options:
--cfg <CONFIG> Directory containing server config files
--password <PASSWORD> The password that encrypts the configs, will prompt if not passed in [env: FM_PASSWORD=]
--password <PASSWORD> The password that encrypts the configs [env: FM_PASSWORD=]
--descriptor <DESCRIPTOR> Wallet descriptor, can be used instead of --cfg
--key <KEY> Wallet secret key, can be used instead of config together with --descriptor
--network <NETWORK> Network to operate on, has to be specified if --cfg isn't present [default: bitcoin]
--readonly Open the database in read-only mode, useful for debugging, should not be used in production
-h, --help Print help
```

Expand Down
29 changes: 22 additions & 7 deletions recoverytool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};
use std::hash::Hasher;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use anyhow::anyhow;
use bitcoin::hashes::hex::FromHex;
Expand All @@ -26,7 +26,7 @@ use fedimint_ln_server::Lightning;
use fedimint_logging::TracingSetup;
use fedimint_mint_server::common::MintCommonInit;
use fedimint_mint_server::Mint;
use fedimint_rocksdb::RocksDb;
use fedimint_rocksdb::{RocksDb, RocksDbReadOnly};
use fedimint_server::config::io::read_server_config;
use fedimint_server::db::SignedSessionOutcomePrefix;
use fedimint_wallet_server::common::config::WalletConfig;
Expand Down Expand Up @@ -67,6 +67,10 @@ struct RecoveryTool {
/// Network to operate on, has to be specified if --cfg isn't present
#[arg(long, default_value = "bitcoin", requires = "descriptor")]
network: Network,
/// Open the database in read-only mode, useful for debugging, should not be
/// used in production
#[arg(long)]
readonly: bool,
#[command(subcommand)]
strategy: TweakSource,
}
Expand Down Expand Up @@ -104,6 +108,20 @@ fn tweak_parser(hex: &str) -> anyhow::Result<[u8; 33]> {
.map_err(|_| anyhow!("tweaks have to be 33 bytes long"))
}

fn get_db(readonly: bool, path: &Path, module_decoders: ModuleDecoderRegistry) -> Database {
if readonly {
Database::new(
RocksDbReadOnly::open_read_only(path).expect("Error opening readonly DB"),
module_decoders,
)
} else {
Database::new(
RocksDb::open(path).expect("Error opening DB"),
module_decoders,
)
}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
TracingSetup::default().init()?;
Expand Down Expand Up @@ -135,10 +153,7 @@ async fn main() -> anyhow::Result<()> {
.expect("Could not encode to stdout")
}
TweakSource::Utxos { legacy, db } => {
let db = Database::new(
RocksDb::open(db).expect("Error opening DB"),
Default::default(),
);
let db = get_db(opts.readonly, &db, Default::default());

let db = if legacy {
db
Expand Down Expand Up @@ -185,7 +200,7 @@ async fn main() -> anyhow::Result<()> {
),
]);

let db = Database::new(RocksDb::open(db).expect("Error opening DB"), decoders);
let db = get_db(opts.readonly, &db, decoders);
let mut dbtx = db.begin_transaction().await;

let mut change_tweak_idx: u64 = 0;
Expand Down
12 changes: 12 additions & 0 deletions scripts/tests/recoverytool-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Runs a simple recoverytool test

set -euo pipefail
export RUST_LOG="${RUST_LOG:-info}"

source scripts/_common.sh
build_workspace
add_target_dir_to_path
make_fm_test_marker

devimint recoverytool-tests
8 changes: 7 additions & 1 deletion scripts/tests/test-ci-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ cargo nextest run --no-run ${CARGO_PROFILE:+--cargo-profile ${CARGO_PROFILE}} ${
# If you really need to break this rule, ping dpc
export FM_CARGO_DENY_COMPILATION=1

function recoverytool_tests() {
fm-run-test "${FUNCNAME[0]}" ./scripts/tests/recoverytool-tests.sh
}
export -f recoverytool_tests

function reconnect_test() {
fm-run-test "${FUNCNAME[0]}" ./scripts/tests/reconnect-test.sh
}
Expand Down Expand Up @@ -121,7 +126,8 @@ if parallel \
lightning_reconnect_test \
gateway_reboot_test \
devimint_cli_test \
load_test_tool_test ; then
load_test_tool_test \
recoverytool_tests ; then
>&2 echo "All tests successful"
else
>&2 echo "Some tests failed. Full job log:"
Expand Down

0 comments on commit 361a16f

Please sign in to comment.