depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

main.rs (17604B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2022-2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     panic::UnwindSafe,
     19     path::{Path, PathBuf},
     20     str::FromStr,
     21     string::String,
     22     sync::{Arc, Mutex},
     23     time::{Duration, Instant},
     24 };
     25 
     26 use bitcoin::{Address, Txid, address::NetworkUnchecked};
     27 use clap::{Parser, ValueEnum};
     28 use depolymerizer_bitcoin::{
     29     CONFIG_SOURCE, DB_SCHEMA,
     30     cli::{Command, run},
     31     config::{WalletCfg, WorkerCfg, parse_db_cfg},
     32     db,
     33     payto::BtcWallet,
     34     rpc::{Error, ErrorCode, Rpc, rpc_common, rpc_wallet},
     35     taler_utils::taler_to_btc,
     36 };
     37 use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
     38 use owo_colors::OwoColorize;
     39 use sqlx::PgPool;
     40 use taler_common::{
     41     api::{EddsaPublicKey, HashCode, ShortHashCode, wire::TransferRequest},
     42     config::Config,
     43     db::pool,
     44     log::taler_logger,
     45     types::{amount::amount, payto::FullPayto},
     46 };
     47 use taler_test_utils::repl::Repl;
     48 use tokio::task::{JoinError, JoinHandle};
     49 use tracing::info;
     50 use tracing_subscriber::util::SubscriberInitExt as _;
     51 use url::Url;
     52 
     53 use crate::utils::{ChildGuard, LocalDb, TestCtx, cmd_redirect, patch_config, try_cmd_redirect};
     54 
     55 mod btc;
     56 mod utils;
     57 
     58 /// Depolymerizer instrumentation test
     59 #[derive(clap::Parser, Debug)]
     60 enum Testbench {
     61     /// Start instrumentation tests for offline testing
     62     Instrumentation {
     63         /// With tests to run
     64         #[clap(global = true, default_value = "")]
     65         filters: Vec<String>,
     66     },
     67     /// Start bitcoin testbench for online testing
     68     Bitcoin { network: Network },
     69 }
     70 
     71 struct Tmp<'a> {
     72     root: &'a Path,
     73     filters: &'a [String],
     74     m: MultiProgress,
     75     start_style: ProgressStyle,
     76     ok_style: ProgressStyle,
     77     err_style: ProgressStyle,
     78     tasks: Vec<(
     79         &'static str,
     80         JoinHandle<(Result<(), JoinError>, Duration, String)>,
     81     )>,
     82     db: Arc<LocalDb>,
     83     start: Instant,
     84 }
     85 
     86 impl<'a> Tmp<'a> {
     87     async fn check<T, F>(&mut self, name: &'static str, task: T)
     88     where
     89         T: FnOnce(TestCtx) -> F + Send + UnwindSafe + 'static,
     90         F: Future<Output = ()> + Send + 'static,
     91     {
     92         if self.filters.is_empty() || self.filters.iter().any(|f| name.starts_with(f)) {
     93             let pb = self.m.add(ProgressBar::new_spinner());
     94             pb.set_style(self.start_style.clone());
     95             pb.set_prefix(name);
     96             pb.set_message("Init");
     97             pb.enable_steady_tick(Duration::from_millis(1000));
     98             let ok_style = self.ok_style.clone();
     99             let err_style = self.err_style.clone();
    100             let start = self.start;
    101             let db = self.db.clone();
    102             let ctx: TestCtx = TestCtx::new(self.root, name, pb.clone(), db);
    103             self.tasks.push((
    104                 name,
    105                 tokio::spawn(async move {
    106                     let result = tokio::spawn(task(ctx)).await;
    107                     if result.is_ok() {
    108                         pb.set_style(ok_style.clone());
    109                         pb.finish_with_message("OK");
    110                     } else {
    111                         pb.set_style(err_style.clone());
    112                         pb.finish();
    113                     }
    114                     (result, start.elapsed(), pb.message())
    115                 }),
    116             ));
    117         }
    118     }
    119 }
    120 
    121 #[tokio::main]
    122 pub async fn main() {
    123     taler_logger(None, false).init();
    124     match Testbench::parse() {
    125         Testbench::Instrumentation { filters } => instrumentation(filters).await,
    126         Testbench::Bitcoin { network } => bitcoin(network).await,
    127     }
    128 }
    129 
    130 #[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
    131 enum Network {
    132     Local,
    133     Test,
    134 }
    135 struct BtcEnv {
    136     network: Network,
    137     db: LocalDb,
    138     bitcoind: ChildGuard,
    139     wire_rpc: Rpc,
    140     client_rpc: Rpc,
    141     cfg: Config,
    142     wire_addr: Address,
    143     client_addr: Address,
    144     tracked: Vec<Txid>,
    145     pool: PgPool,
    146 }
    147 
    148 impl BtcEnv {
    149     pub async fn init(network: Network) -> Self {
    150         let network_dir = match network {
    151             Network::Local => "btc-local",
    152             Network::Test => "btc-test",
    153         };
    154         println!("Setup bitcoin {network:?} env in {network_dir}");
    155         let root_dir = PathBuf::from_str("./testbench/env")
    156             .unwrap()
    157             .join(network_dir);
    158         std::fs::create_dir_all(root_dir.join("bitcoin")).unwrap();
    159         let root_dir = root_dir.canonicalize().unwrap();
    160 
    161         // Generate bitcoind config
    162         let cfg = match network {
    163             Network::Local => {
    164                 "
    165                 chain=regtest
    166             txindex=1
    167             rpcservertimeout=0
    168             fallbackfee=0.00000001
    169 
    170             [regtest]
    171             rpcport=18345
    172             "
    173             }
    174             Network::Test => {
    175                 "
    176             chain=signet
    177             txindex=1
    178             rpcservertimeout=0
    179             fallbackfee=0.000001
    180             [signet]
    181             rpcport=18345
    182             "
    183             }
    184         };
    185         std::fs::write(root_dir.join("bitcoin/bitcoin.conf"), cfg).unwrap();
    186         let datadir = match network {
    187             Network::Local => root_dir.join("bitcoin").join("regtest"),
    188             Network::Test => root_dir.join("bitcoin").join("signet"),
    189         };
    190 
    191         // Start database
    192         let db = LocalDb::new(&root_dir);
    193 
    194         // Generate Taler config
    195         let taler_cfg_path = root_dir.join("taler.conf");
    196         patch_config(
    197             "testbench/conf/taler_btc_dev.conf",
    198             &taler_cfg_path,
    199             |ini| {
    200                 ini.with_section(Some("depolymerizer-bitcoin-worker"))
    201                     .set("RPC_COOKIE_FILE", datadir.to_string_lossy());
    202                 ini.with_section(Some("depolymerizer-bitcoindb-postgres"))
    203                     .set("CONFIG", db.postgres_uri("depolymerizer_bitcoin"));
    204             },
    205         );
    206 
    207         // Start node
    208         let bitcoind = cmd_redirect(
    209             "bitcoind",
    210             &[&format!(
    211                 "-datadir={}",
    212                 root_dir.join("bitcoin").to_string_lossy()
    213             )],
    214             root_dir.join("bitcoind.log"),
    215         );
    216         let cfg = Config::load(CONFIG_SOURCE, Some(&taler_cfg_path)).unwrap();
    217         let worker_cfg = WorkerCfg::parse(&cfg).unwrap();
    218         let mut rpc = retry_opt!(rpc_common(&worker_cfg.rpc_cfg));
    219 
    220         for wallet in ["wire", "client"] {
    221             loop {
    222                 let res = rpc.load_wallet(wallet).await;
    223                 if let Err(Error::RPC { code, .. }) = res {
    224                     match code {
    225                         ErrorCode::RpcInWarmup => continue,
    226                         ErrorCode::RpcWalletNotFound => {
    227                             rpc.create_wallet(wallet, "").await.unwrap();
    228                             break;
    229                         }
    230                         _ => {
    231                             res.unwrap();
    232                         }
    233                     }
    234                 } else {
    235                     break;
    236                 }
    237             }
    238         }
    239         drop(rpc);
    240         let mut wire_rpc = rpc_wallet(
    241             &worker_cfg.rpc_cfg,
    242             &WalletCfg {
    243                 name: "wire".to_string(),
    244                 password: None,
    245             },
    246         )
    247         .await
    248         .unwrap();
    249         let mut client_rpc = rpc_wallet(
    250             &worker_cfg.rpc_cfg,
    251             &WalletCfg {
    252                 name: "client".to_string(),
    253                 password: None,
    254             },
    255         )
    256         .await
    257         .unwrap();
    258         let wire_addr = wire_rpc.gen_addr().await.unwrap();
    259         let client_addr = client_rpc.gen_addr().await.unwrap();
    260         patch_config(&taler_cfg_path, &taler_cfg_path, |ini| {
    261             ini.with_section(Some("depolymerizer-bitcoin"))
    262                 .set("WALLET", wire_addr.to_string());
    263         });
    264         let cfg = Config::load(CONFIG_SOURCE, Some(taler_cfg_path)).unwrap();
    265 
    266         // Wait for db to start
    267         db.wait_running();
    268         db.create_db("depolymerizer_bitcoin");
    269 
    270         // Dbinit & setup
    271         run(Command::Dbinit { reset: false }, &cfg).await.unwrap();
    272         run(Command::Setup { reset: false }, &cfg).await.unwrap();
    273 
    274         let db_cg = parse_db_cfg(&cfg).unwrap();
    275         Self {
    276             pool: pool(db_cg.cfg, DB_SCHEMA).await.unwrap(),
    277             wire_addr,
    278             client_addr,
    279             network,
    280             db,
    281             bitcoind,
    282             cfg,
    283             wire_rpc,
    284             client_rpc,
    285             tracked: Vec::new(),
    286         }
    287     }
    288 }
    289 
    290 #[derive(clap::Parser, Debug)]
    291 #[command(no_binary_name = true)]
    292 enum Shell {
    293     Setup,
    294     Reset,
    295     ResetDb,
    296     Sync,
    297     Credit,
    298     Debit,
    299     Mine {
    300         amount: Option<u16>,
    301         addr: Option<Address<NetworkUnchecked>>,
    302     },
    303     Exit,
    304     Tx {
    305         txid: Txid,
    306     },
    307     Track {
    308         txid: Txid,
    309     },
    310     Untrack {
    311         txid: Txid,
    312     },
    313 }
    314 
    315 async fn bitcoin(network: Network) {
    316     let mut env = BtcEnv::init(network).await;
    317 
    318     let mut repl = Repl::new(".history");
    319     loop {
    320         let info = env.client_rpc.get_blockchain_info().await.unwrap();
    321         let wire_balance = env.wire_rpc.get_balance().await.unwrap();
    322         println!("wire {} {wire_balance}", env.wire_addr);
    323         let client_balance = env.client_rpc.get_balance().await.unwrap();
    324         println!("client {} {client_balance}", env.client_addr);
    325         for txid in &env.tracked {
    326             match env.client_rpc.get_tx(txid).await {
    327                 Ok(info) => println!(
    328                     "{} {txid} {} {}",
    329                     "tx".cyan(),
    330                     info.amount,
    331                     info.confirmations
    332                 ),
    333                 Err(e) => println!("{} {txid} {}", "tx".cyan(), e.red()),
    334             }
    335         }
    336         if let Some(cmd) =
    337             repl.read_line(&info.chain, &format!("{:.6}", info.verification_progress))
    338         {
    339             match cmd {
    340                 Shell::Setup => run(Command::Setup { reset: false }, &env.cfg)
    341                     .await
    342                     .unwrap(),
    343                 Shell::Reset => run(Command::Setup { reset: true }, &env.cfg).await.unwrap(),
    344                 Shell::ResetDb => {
    345                     run(Command::Dbinit { reset: true }, &env.cfg)
    346                         .await
    347                         .unwrap();
    348                     run(Command::Setup { reset: false }, &env.cfg)
    349                         .await
    350                         .unwrap();
    351                 }
    352                 Shell::Sync => run(Command::Worker { transient: true }, &env.cfg)
    353                     .await
    354                     .unwrap(),
    355                 Shell::Credit => {
    356                     let reserve_pub = EddsaPublicKey::rand();
    357                     let amount = amount("DEVBTC:0.00011");
    358                     let txid = env
    359                         .client_rpc
    360                         .send_segwit_key(&env.wire_addr, &taler_to_btc(&amount), &reserve_pub)
    361                         .await
    362                         .unwrap();
    363                     env.tracked.push(txid);
    364                     info!(target: "testbench", "Credit {reserve_pub} {amount} {txid} to {}", env.wire_addr);
    365                 }
    366                 Shell::Debit => {
    367                     let creditor = FullPayto::new(BtcWallet(env.client_addr.clone()), "client");
    368                     let wtid = ShortHashCode::rand();
    369                     let amount = amount("DEVBTC:0.0001");
    370                     let transfer = TransferRequest {
    371                         request_uid: HashCode::rand(),
    372                         amount,
    373                         exchange_base_url: Url::parse("https://test.com/").unwrap(),
    374                         wtid: wtid.clone(),
    375                         credit_account: creditor.as_uri(),
    376                         metadata: None,
    377                     };
    378                     db::transfer(&env.pool, &creditor, &transfer).await.unwrap();
    379                     info!(target: "testbench", "Debit {wtid} {amount} to {}", env.wire_addr);
    380                 }
    381                 Shell::Mine { amount, addr } => {
    382                     let amount = amount.unwrap_or(1);
    383                     let addr = addr.map(|a| a.assume_checked());
    384                     let addr = addr.as_ref().unwrap_or(&env.client_addr);
    385                     env.client_rpc.mine(amount, addr).await.unwrap();
    386                 }
    387                 Shell::Exit => break,
    388                 Shell::Tx { txid } => {
    389                     let info = env.client_rpc.get_tx(&txid).await.unwrap();
    390                     info!(target: "testbench", "{txid} {} {}", info.amount, info.confirmations);
    391                 }
    392                 Shell::Track { txid } => {
    393                     env.tracked.push(txid);
    394                 }
    395                 Shell::Untrack { txid } => {
    396                     env.tracked.retain(|id| *id != txid);
    397                 }
    398             }
    399         } else {
    400             break;
    401         }
    402     }
    403 }
    404 
    405 pub async fn instrumentation(filters: Vec<String>) {
    406     let root = PathBuf::from_str("testbench/instrumentation")
    407         .unwrap()
    408         .canonicalize()
    409         .unwrap();
    410     std::fs::remove_dir_all(&root).ok();
    411     std::fs::create_dir_all(root.join("bin")).unwrap();
    412 
    413     // Set panic hook
    414     let failures = Arc::new(Mutex::new(Vec::new()));
    415     {
    416         let failures = failures.clone();
    417         std::panic::set_hook(Box::new(move |e| {
    418             let backtrace = std::backtrace::Backtrace::force_capture();
    419             let info = format!("{e}\n{backtrace}");
    420             if let Some(id) = tokio::task::try_id() {
    421                 failures.lock().unwrap().push((id, info));
    422             } else {
    423                 eprintln!("Failed outside of a task:\n{info}")
    424             }
    425         }));
    426     }
    427 
    428     // Build binaries
    429     let p = ProgressBar::new_spinner();
    430     p.set_style(ProgressStyle::with_template("building {msg} {elapsed:.dim}").unwrap());
    431     p.enable_steady_tick(Duration::from_millis(1000));
    432     for name in ["depolymerizer-bitcoin"] {
    433         build_bin(&root, &p, name, None, name);
    434         build_bin(&root, &p, name, Some("fail"), &format!("{name}-fail"));
    435     }
    436     p.finish_and_clear();
    437 
    438     // Run tests
    439     let m = MultiProgress::new();
    440     let start_style =
    441         ProgressStyle::with_template("{prefix:.magenta} {msg} {elapsed:.dim}").unwrap();
    442     let ok_style =
    443         ProgressStyle::with_template("{prefix:.magenta} {msg:.green} {elapsed:.dim}").unwrap();
    444     let err_style =
    445         ProgressStyle::with_template("{prefix:.magenta} {msg:.red} {elapsed:.dim}").unwrap();
    446 
    447     let start = Instant::now();
    448     let db = Arc::new(LocalDb::new(&root));
    449     let mut tmp = Tmp {
    450         root: &root,
    451         filters: filters.as_slice(),
    452         m,
    453         start_style,
    454         ok_style,
    455         err_style,
    456         tasks: Vec::new(),
    457         db,
    458         start,
    459     };
    460     tmp.check("btc_wire", btc::wire).await;
    461     tmp.check("btc_lifetime", btc::lifetime).await;
    462     tmp.check("btc_reconnect", btc::reconnect).await;
    463     tmp.check("btc_stress", btc::stress).await;
    464     tmp.check("btc_conflict", btc::conflict).await;
    465     tmp.check("btc_reorg", btc::reorg).await;
    466     tmp.check("btc_hell", btc::hell).await;
    467     tmp.check("btc_analysis", btc::analysis).await;
    468     tmp.check("btc_bumpfee", btc::bumpfee).await;
    469     tmp.check("btc_maxfee", btc::maxfee).await;
    470     tmp.check("btc_config", btc::config).await;
    471     let mut results = Vec::new();
    472     for (name, task) in tmp.tasks {
    473         results.push((name, task.await.unwrap()));
    474     }
    475 
    476     let len = results.len();
    477 
    478     tmp.m.clear().unwrap();
    479     let failures = failures.lock().unwrap();
    480     for (name, (result, _, msg)) in &results {
    481         if let Err(e) = result
    482             && let Some((_, err)) = failures.iter().find(|(id, _)| *id == e.id())
    483         {
    484             println!("{} {}\n{}", name.magenta(), msg.red(), err.bright_black());
    485         }
    486     }
    487     for (name, (result, time, msg)) in results {
    488         match result {
    489             Ok(_) => {
    490                 println!(
    491                     "{} {} {}",
    492                     name.magenta(),
    493                     "OK".green(),
    494                     format_args!("{}s", time.as_secs()).bright_black()
    495                 );
    496             }
    497             Err(_) => {
    498                 println!(
    499                     "{} {} {}",
    500                     name.magenta(),
    501                     msg.red(),
    502                     format_args!("{}s", time.as_secs()).bright_black()
    503                 );
    504             }
    505         }
    506     }
    507     println!("{} tests in {}s", len, start.elapsed().as_secs());
    508 }
    509 
    510 pub fn build_bin(root: &Path, p: &ProgressBar, name: &str, features: Option<&str>, bin_name: &str) {
    511     p.set_message(bin_name.to_string());
    512     let mut args = vec!["build", "--bin", name, "--release"];
    513     if let Some(features) = features {
    514         args.extend_from_slice(&["--features", features]);
    515     }
    516     let result = try_cmd_redirect("cargo", &args, root.join("bin/build"))
    517         .unwrap()
    518         .0
    519         .wait()
    520         .unwrap();
    521     assert!(result.success());
    522     std::fs::rename(
    523         format!("target/release/{name}"),
    524         root.join("bin").join(bin_name),
    525     )
    526     .unwrap();
    527 }