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 }