btc-harness.rs (37445B)
1 /* 2 This file is part of TALER 3 Copyright (C) 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 assert_matches, 19 collections::BTreeSet, 20 net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener}, 21 process::{Child, Stdio}, 22 str::FromStr, 23 time::Duration, 24 }; 25 26 use bitcoin::{Address, Amount, Txid, address::NetworkUnchecked, hashes::Hash}; 27 use clap::Parser as _; 28 use depolymerizer_bitcoin::{ 29 RpcApiExtended as _, 30 config::{RpcAuth, RpcCfg, WorkerCfg}, 31 constants::CONFIG_SOURCE, 32 db::{self, dbinit, incoming_history, pool}, 33 loops::{LoopError, LoopResult, analysis::analysis, worker::worker_step}, 34 payto::{BtcWallet, FullBtcPayto}, 35 rpc::{Category, Error, ErrorCode, Rpc, RpcApi, WalletRpc}, 36 setup::setup, 37 taler_utils::{btc_to_taler, taler_to_btc}, 38 }; 39 use failure_injection::set_failure_scenario; 40 use owo_colors::OwoColorize as _; 41 use sqlx::PgPool; 42 use taler_api::notification::dummy_listen; 43 use taler_build::long_version; 44 use taler_common::{ 45 CommonArgs, 46 api::{ 47 EddsaPublicKey, HashCode, ShortHashCode, 48 params::{History, Page, Pooling}, 49 wire::{IncomingBankTransaction, TransferRequest, TransferState}, 50 }, 51 config::Config, 52 taler_main, 53 types::{ 54 amount::{Currency, amount}, 55 url, 56 }, 57 }; 58 use taler_test_utils::repl::Repl; 59 use tracing::info; 60 61 #[derive(Debug)] 62 struct PanicErr; 63 64 type PanicRes<T> = Result<T, PanicErr>; 65 66 impl<E: std::fmt::Display> From<E> for PanicErr { 67 #[track_caller] 68 fn from(value: E) -> Self { 69 panic!("{value}") 70 } 71 } 72 73 /// Bictoin Adapter harness test suite 74 #[derive(clap::Parser, Debug)] 75 #[command(long_version = long_version(), about, long_about = None)] 76 struct Args { 77 #[clap(flatten)] 78 common: CommonArgs, 79 80 #[command(subcommand)] 81 cmd: Command, 82 } 83 84 #[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)] 85 enum Network { 86 Local, 87 Test, 88 } 89 90 #[derive(clap::Subcommand, Debug)] 91 enum Command { 92 /// Run logic tests 93 Logic { 94 #[arg(short, long)] 95 reset: bool, 96 }, 97 /// Run online tests 98 Online { 99 #[arg(short, long)] 100 reset: bool, 101 network: Network, 102 }, 103 } 104 105 fn step(step: &str) { 106 println!("{}", step.green()); 107 } 108 109 #[derive(Debug)] 110 struct SystemCtx { 111 n1_dir: String, 112 n2_dir: String, 113 c_dir: String, 114 peer_addr: SocketAddr, 115 n1_addr: SocketAddr, 116 n2_addr: SocketAddr, 117 n1_cookie: String, 118 n2_cookie: String, 119 } 120 121 impl SystemCtx { 122 fn new(reset: bool) -> Self { 123 // Prepare dir 124 let dir = "depolymerizer-bitcoin/test/logic"; 125 std::fs::create_dir_all(dir).unwrap(); 126 if reset { 127 std::fs::remove_dir_all(dir).unwrap(); 128 } 129 let n1_dir = format!("{dir}/n1"); 130 let n2_dir = format!("{dir}/n2"); 131 let c_dir = format!("{dir}/c"); 132 for dir in [&n1_dir, &n2_dir] { 133 std::fs::create_dir_all(dir).unwrap(); 134 } 135 136 // Choose unused port 137 let btc_port = unused_port(); 138 let btc_rpc_port = unused_port(); 139 let btc2_port = unused_port(); 140 let btc2_rpc_port = unused_port(); 141 142 // Bitcoin config 143 for (root, btc, rpc) in [ 144 (&n1_dir, btc_port, btc_rpc_port), 145 (&n2_dir, btc2_port, btc2_rpc_port), 146 ] { 147 std::fs::write( 148 format!("{root}/bitcoin.conf"), 149 format!( 150 " 151 regtest=1 152 txindex=1 153 maxtxfee=0.01 154 fallbackfee=0.00000001 155 rpcservertimeout=0 156 dbcache=4 157 maxmempool=5 158 par=2 159 rpcthreads=5 160 161 [regtest] 162 bind=127.0.0.1:{btc} 163 rpcport={rpc} 164 " 165 ), 166 ) 167 .unwrap(); 168 } 169 170 Self { 171 n1_addr: SocketAddrV4::new(Ipv4Addr::LOCALHOST, btc_rpc_port).into(), 172 n1_cookie: format!("{n1_dir}/regtest/.cookie"), 173 n2_addr: SocketAddrV4::new(Ipv4Addr::LOCALHOST, btc2_rpc_port).into(), 174 n2_cookie: format!("{n2_dir}/regtest/.cookie"), 175 peer_addr: SocketAddrV4::new(Ipv4Addr::LOCALHOST, btc2_port).into(), 176 n1_dir, 177 n2_dir, 178 c_dir, 179 } 180 } 181 182 async fn test_cfg(&self, btc_patch: &str, cfg_patch: &str) { 183 let port = unused_port(); 184 let Self { c_dir, .. } = self; 185 std::fs::remove_dir_all(c_dir).ok(); 186 std::fs::create_dir_all(c_dir).unwrap(); 187 std::fs::write( 188 format!("{c_dir}/bitcoin.conf"), 189 format!( 190 " 191 regtest=1 192 txindex=1 193 maxtxfee=0.01 194 fallbackfee=0.00000001 195 rpcservertimeout=0 196 dbcache=4 197 maxmempool=5 198 par=2 199 rpcthreads=5 200 201 [regtest] 202 rpcport={port} 203 204 {btc_patch} 205 " 206 ), 207 ) 208 .unwrap(); 209 let cfg = Config::from_mem_with_env( 210 CONFIG_SOURCE, 211 &format!( 212 " 213 [depolymerizer-bitcoin-worker] 214 RPC_BIND=127.0.0.1:{port} 215 216 {cfg_patch} 217 " 218 ), 219 ) 220 .unwrap(); 221 let mut n = cmd("bitcoind", &[format!("-datadir={}", self.c_dir).as_str()]); 222 let cfg = RpcCfg::parse(&cfg).unwrap(); 223 wait_up(&cfg).await; 224 n.0.kill().unwrap(); 225 n.0.wait().unwrap(); 226 } 227 228 async fn start_n1(&self, args: &[&str]) -> (ChildGuard, Rpc) { 229 let Self { 230 n1_dir, 231 n1_addr, 232 n1_cookie, 233 .. 234 } = self; 235 let n = cmd( 236 "bitcoind", 237 Vec::from_iter( 238 [format!("-datadir={n1_dir}").as_str()] 239 .iter() 240 .chain(args) 241 .copied(), 242 ) 243 .as_slice(), 244 ); 245 let mut rpc = wait_up(&RpcCfg { 246 addr: *n1_addr, 247 auth: RpcAuth::Cookie(n1_cookie.clone()), 248 }) 249 .await; 250 for name in ["wire", "client", "reserve"] { 251 if let Err(e) = rpc.load_wallet(name).await { 252 if let Error::RPC { 253 code: ErrorCode::RpcWalletNotFound, 254 .. 255 } = e 256 { 257 rpc.create_wallet(name, "").await.unwrap(); 258 } else { 259 break; 260 } 261 } 262 } 263 (n, rpc) 264 } 265 266 async fn start_n2(&self, rpc1: &mut impl RpcApi) -> (ChildGuard, Rpc) { 267 let n = cmd("bitcoind", &[&format!("-datadir={}", self.n2_dir)]); 268 let rpc = wait_up(&RpcCfg { 269 addr: self.n2_addr, 270 auth: RpcAuth::Cookie(self.n2_cookie.clone()), 271 }) 272 .await; 273 rpc1.add_node(&self.peer_addr.to_string()).await.unwrap(); 274 (n, rpc) 275 } 276 } 277 278 struct Ctx<'a> { 279 sc: &'a SystemCtx, 280 n1: &'a mut ChildGuard, 281 pool: &'a PgPool, 282 rpc: Rpc, 283 rpc2: Rpc, 284 wire_addr: &'a Address, 285 client_addr: &'a Address, 286 reserve_addr: &'a Address, 287 s: WorkerCfg, 288 status: bool, 289 } 290 291 impl<'a> Ctx<'a> { 292 /// Run the worker once 293 async fn run_worker(&mut self) -> LoopResult<()> { 294 let db = &mut self.pool.acquire().await.unwrap().detach(); 295 let res = worker_step( 296 db, 297 &mut self.rpc.wallet("wire"), 298 &mut self.s, 299 &mut self.status, 300 ) 301 .await; 302 if let Err(e) = &res { 303 tracing::error!(target: "worker", "{e}"); 304 } 305 res 306 } 307 308 /// Run the worker once successfully 309 async fn worker(&mut self) { 310 self.run_worker().await.unwrap(); 311 } 312 313 /// Run the worker once and fail on an injected error 314 async fn w_injected_err(&mut self) { 315 assert_matches!(self.run_worker().await, Err(LoopError::Injected(_))); 316 } 317 318 /// Run the worker once and fail on a rpc error 319 async fn w_rpc_err(&mut self) { 320 assert_matches!( 321 self.run_worker().await, 322 Err(LoopError::Rpc(Error::RPC { .. })) 323 ); 324 } 325 326 async fn restart_node(&mut self, args: &[&str]) { 327 // Stop node cleanly 328 self.rpc.stop().await.unwrap(); 329 self.n1.0.wait().unwrap(); 330 // Start a new node 331 let (n, rpc) = self.sc.start_n1(args).await; 332 *self.n1 = n; 333 self.rpc = rpc; 334 self.rpc 335 .add_node(&self.sc.peer_addr.to_string()) 336 .await 337 .unwrap(); 338 } 339 340 async fn cluster_deco(&mut self) { 341 self.rpc 342 .disconnect_node(&self.sc.peer_addr.to_string()) 343 .await 344 .unwrap(); 345 } 346 347 async fn cluster_fork(&mut self) -> u32 { 348 let n1h = self.rpc.get_blockchain_info().await.unwrap().blocks; 349 let n2h = self.rpc2.get_blockchain_info().await.unwrap().blocks; 350 let diff = n1h - n2h; 351 self.rpc2.mine(diff + 1, self.reserve_addr).await.unwrap(); 352 self.rpc 353 .add_node(&self.sc.peer_addr.to_string()) 354 .await 355 .unwrap(); 356 self.rpc.wait_for_block_height(n1h + 1).await.unwrap(); 357 diff + 2 358 } 359 360 async fn forget_tx(&self, tx: &Txid) { 361 sqlx::query("UPDATE tx_in SET txid=NULL WHERE txid=$1") 362 .bind(tx.as_byte_array()) 363 .execute(self.pool) 364 .await 365 .unwrap(); 366 } 367 368 /* ----- Rpc ----- */ 369 370 fn client(&mut self) -> WalletRpc<'_> { 371 self.rpc.wallet("client") 372 } 373 374 fn wire(&mut self) -> WalletRpc<'_> { 375 self.rpc.wallet("wire") 376 } 377 378 fn reserve(&mut self) -> WalletRpc<'_> { 379 self.rpc.wallet("reserve") 380 } 381 382 /* ----- Balance ---- */ 383 384 async fn c_balance(&mut self) -> Amount { 385 self.client().get_balance().await.unwrap() 386 } 387 388 async fn w_balance(&mut self) -> Amount { 389 self.wire().get_balance().await.unwrap() 390 } 391 392 async fn expect_c_balance(&mut self, amount: Amount) { 393 assert_eq!(self.c_balance().await, amount) 394 } 395 396 async fn expect_w_balance(&mut self, amount: Amount) { 397 assert_eq!(self.w_balance().await, amount) 398 } 399 400 /* ----- Mining ----- */ 401 402 /// Mine nb block 403 async fn mine(&mut self, nb: u32) { 404 self.rpc.mine(nb, self.reserve_addr).await.unwrap(); 405 } 406 407 /// Mine enough block to reach confirmation 408 async fn next_conf(&mut self) { 409 self.mine(self.s.conf).await 410 } 411 412 /// Mine one block 413 async fn next_block(&mut self) { 414 self.mine(1).await 415 } 416 417 /// Mine while txs are pending 418 async fn mine_pending(&mut self) { 419 while self.rpc.get_mempool_info().await.unwrap().size > 0 { 420 self.mine(1).await 421 } 422 } 423 424 /// Mine while txs are pending or unconfirmed 425 async fn mine_conf(&mut self) { 426 self.mine_pending().await; 427 self.next_conf().await; 428 } 429 430 /* ----- Transaction ----- */ 431 432 async fn withdrawal(&mut self, amount: Amount) -> (Txid, EddsaPublicKey) { 433 let key = EddsaPublicKey::rand(); 434 let id = self.withdrawal_with_key(amount, &key).await; 435 (id, key) 436 } 437 438 async fn withdrawal_with_key(&mut self, amount: Amount, key: &EddsaPublicKey) -> Txid { 439 loop { 440 match self 441 .rpc 442 .wallet("client") 443 .send_segwit_key(self.wire_addr, amount, key) 444 .await 445 { 446 Ok(id) => return id, 447 Err(e) => match e { 448 Error::RPC { 449 code: ErrorCode::RpcWalletError, 450 .. 451 } => { 452 self.mine(1).await; 453 } 454 _ => panic!("{e:?}"), 455 }, 456 } 457 } 458 } 459 460 async fn transfer_to(&mut self, addr: &Address, amount: Amount) -> u64 { 461 let payto = FullBtcPayto::new(BtcWallet(addr.clone()), "Bitcoin Client"); 462 match db::transfer( 463 self.pool, 464 &payto, 465 &TransferRequest { 466 request_uid: HashCode::rand(), 467 amount: btc_to_taler(&amount.to_signed().unwrap(), &Currency::KUDOS), 468 exchange_base_url: url("https://test.com"), 469 metadata: None, 470 wtid: ShortHashCode::rand(), 471 credit_account: payto.as_uri(), 472 }, 473 ) 474 .await 475 .unwrap() 476 { 477 db::TransferResult::Success(it) => it.row_id, 478 it => panic!("{it:?}"), 479 } 480 } 481 482 async fn transfer(&mut self, amount: Amount) -> u64 { 483 self.transfer_to(self.client_addr, amount).await 484 } 485 486 async fn c_send(&mut self, addr: &Address, amount: Amount) -> Txid { 487 self.rpc 488 .wallet("client") 489 .send(addr, amount, None, false) 490 .await 491 .unwrap() 492 } 493 494 async fn malformed_credit(&mut self, amount: Amount) -> Txid { 495 self.c_send(self.wire_addr, amount).await 496 } 497 498 async fn abandon(mut rpc: impl RpcApi) { 499 let list = rpc.list_since_block(None, 1).await.unwrap(); 500 for tx in list.transactions { 501 if tx.category == Category::Send && tx.confirmations == 0 { 502 rpc.abandon_tx(&tx.txid).await.unwrap(); 503 } 504 } 505 } 506 507 async fn abandon_wire(&mut self) { 508 Self::abandon(self.wire()).await; 509 } 510 511 async fn abandon_client(&mut self) { 512 Self::abandon(self.client()).await; 513 } 514 515 /* ----- Checks ----- */ 516 517 async fn expect_incoming(&self, key: EddsaPublicKey) { 518 let transfer = incoming_history( 519 self.pool, 520 &History { 521 page: Page { 522 limit: -1, 523 offset: None, 524 }, 525 pooling: Pooling { timeout_ms: None }, 526 }, 527 &Currency::TEST, 528 dummy_listen, 529 ) 530 .await 531 .unwrap(); 532 assert_matches!( 533 transfer.first().unwrap(), 534 IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key, 535 ); 536 } 537 538 async fn expect_transfer_status(&self, id: u64, status: TransferState) { 539 let mut attempts = 0; 540 loop { 541 let transfer = db::transfer_by_id(self.pool, id, &Currency::KUDOS) 542 .await 543 .unwrap() 544 .unwrap(); 545 if (transfer.status, transfer.status_msg.as_deref()) == (status, None) { 546 return; 547 } 548 if attempts > 40 { 549 assert_eq!( 550 (transfer.status, transfer.status_msg.as_deref()), 551 (status, None) 552 ); 553 } 554 attempts += 1; 555 tokio::time::sleep(Duration::from_millis(200)).await; 556 } 557 } 558 } 559 560 fn unused_port() -> u16 { 561 TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)) 562 .unwrap() 563 .local_addr() 564 .unwrap() 565 .port() 566 } 567 568 async fn wait_up(cfg: &RpcCfg) -> Rpc { 569 for _ in 0..100 { 570 if let Ok(mut rpc) = Rpc::new(cfg).await 571 && rpc.get_blockchain_info().await.is_ok() 572 { 573 return rpc; 574 } 575 tokio::time::sleep(Duration::from_millis(100)).await; 576 } 577 let mut rpc = Rpc::new(cfg).await.unwrap(); 578 rpc.get_blockchain_info().await.unwrap(); 579 rpc 580 } 581 582 struct ChildGuard(pub Child); 583 584 impl Drop for ChildGuard { 585 fn drop(&mut self) { 586 self.0.kill().ok(); 587 } 588 } 589 590 #[track_caller] 591 fn cmd(cmd: &str, args: &[&str]) -> ChildGuard { 592 let child = std::process::Command::new(cmd) 593 .args(args) 594 .stderr(Stdio::null()) 595 .stdout(Stdio::null()) 596 .stdin(Stdio::null()) 597 .spawn() 598 .unwrap(); 599 ChildGuard(child) 600 } 601 602 /// Run logic tests against local regtest bitcoin chain 603 async fn logic_harness(reset: bool) -> PanicRes<()> { 604 step("Run Bitcoin logic harness tests"); 605 606 step("Start bitcoin network"); 607 let sc = SystemCtx::new(reset); 608 609 // Start bitcoin nodes 610 let (mut n1, mut rpc) = sc.start_n1(&[]).await; 611 let (mut n2, rpc2) = sc.start_n2(&mut rpc).await; 612 let reserve_addr = &rpc.wallet("reserve").gen_addr().await?; 613 let client_addr = &rpc.wallet("client").gen_addr().await?; 614 let wire_addr = &rpc.wallet("wire").gen_addr().await?; 615 616 let cfg = Config::from_mem_with_env( 617 CONFIG_SOURCE, 618 &format!( 619 " 620 [depolymerizer-bitcoin] 621 CURRENCY = BTC 622 NAME = Exchange Owner 623 WALLET = {wire_addr} 624 625 [depolymerizer-bitcoin-worker] 626 BOUNCE_FEE = BTC:0.00001 627 CONFIRMATION = 3 628 RPC_BIND = {} 629 WALLET_NAME = wire 630 RPC_COOKIE_FILE = {} 631 ", 632 sc.n1_addr, sc.n1_cookie 633 ), 634 )?; 635 636 step("Setup"); 637 dbinit(&cfg, reset).await?; 638 setup(&cfg, reset).await?; 639 let state = WorkerCfg::parse(&cfg)?; 640 let pool = &pool(&cfg).await?; 641 642 let c = &mut Ctx { 643 sc: &sc, 644 n1: &mut n1, 645 pool, 646 rpc, 647 rpc2, 648 client_addr, 649 wire_addr, 650 reserve_addr, 651 s: state, 652 status: true, 653 }; 654 655 if c.client().get_balance().await? < Amount::ONE_BTC { 656 while c.reserve().get_balance().await? < Amount::ONE_BTC { 657 c.mine(1).await; 658 } 659 c.reserve() 660 .send(client_addr, Amount::ONE_BTC, None, false) 661 .await 662 .unwrap(); 663 } 664 665 let fee = c.s.bounce_fee; 666 667 step("Warmup"); 668 c.worker().await; 669 c.mine_conf().await; 670 c.worker().await; 671 672 step("Withdrawal"); 673 let amount = Amount::from_sat(330000); 674 let b = c.wire().get_balance().await?; 675 // Send tx 676 let (_, reserve_pub) = c.withdrawal(amount).await; 677 // Confirm tx 678 c.mine_conf().await; 679 // Check received 680 c.expect_w_balance(b + amount).await; 681 // Work register it 682 c.worker().await; 683 // Check registered 684 c.expect_incoming(reserve_pub).await; 685 686 step("Deposit"); 687 let amount = Amount::from_sat(34000); 688 let b = c.c_balance().await; 689 // Request tx 690 let id = c.transfer(amount).await; 691 // Check start pending 692 c.expect_transfer_status(id, TransferState::pending).await; 693 // Send it 694 c.worker().await; 695 // Mine it 696 c.mine_pending().await; 697 // Client already received the fund 698 c.expect_c_balance(b + amount).await; 699 // Still pending until confirmed 700 c.worker().await; 701 c.expect_transfer_status(id, TransferState::pending).await; 702 // Confirm it 703 c.mine_conf().await; 704 c.worker().await; 705 // Now success 706 c.expect_transfer_status(id, TransferState::success).await; 707 c.expect_c_balance(b + amount).await; 708 709 step("Bounce"); 710 let b = c.w_balance().await; 711 let amount = Amount::from_sat(10000); 712 // Send malformed 713 c.malformed_credit(amount).await; 714 // Ignore until confirmed 715 c.mine_conf().await; 716 c.expect_w_balance(b + amount).await; 717 // Confirm it & bounce 718 c.mine_conf().await; 719 c.worker().await; 720 // Keep fee 721 c.expect_w_balance(b + fee).await; 722 // Confirm the bounce 723 c.mine_conf().await; 724 c.worker().await; 725 726 // TODO lifetime ? 727 // TODO reconnect rpc and database? 728 729 step("Transfer to unknown account"); 730 c.transfer_to( 731 &Address::<NetworkUnchecked>::from_str("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh") 732 .unwrap() 733 .assume_checked(), 734 Amount::from_sat(34100), 735 ) 736 .await; 737 c.worker().await; 738 739 step("Debit failure"); 740 set_failure_scenario(&["debit"]); 741 let amount = Amount::from_sat(34000); 742 let b = c.c_balance().await; 743 let id = c.transfer(amount).await; 744 // Check idempotent 745 c.w_injected_err().await; 746 c.worker().await; 747 // Confirm it 748 c.mine_conf().await; 749 c.worker().await; 750 c.expect_transfer_status(id, TransferState::success).await; 751 // Check only sent once 752 c.expect_c_balance(b + amount).await; 753 754 step("Bounce failure"); 755 set_failure_scenario(&["bounce"]); 756 let b = c.w_balance().await; 757 let amount = Amount::from_sat(20000); 758 // Send malformed 759 c.malformed_credit(amount).await; 760 c.mine_pending().await; 761 // Ignore until confirmed 762 c.mine_conf().await; 763 c.expect_w_balance(b + amount).await; 764 // Check idempotent 765 c.w_injected_err().await; 766 c.worker().await; 767 // Kept fee 768 c.expect_w_balance(b + fee).await; 769 c.mine_conf().await; 770 c.worker().await; 771 c.expect_w_balance(b + fee).await; 772 773 step("Conflict send"); 774 let amount = Amount::from_sat(35000); 775 let b = c.c_balance().await; 776 // Perform deposit 777 c.transfer(amount).await; 778 c.worker().await; 779 // Abandon pending transaction 780 c.restart_node(&["-minrelaytxfee=0.0001"]).await; 781 c.abandon_wire().await; 782 c.expect_c_balance(b).await; 783 // Generate conflict 784 let conflict = Amount::from_sat(40000); 785 c.transfer(conflict).await; 786 c.worker().await; 787 c.mine_pending().await; 788 c.expect_c_balance(b + conflict).await; 789 // Handle conflict 790 c.worker().await; 791 c.mine_pending().await; 792 c.expect_c_balance(b + amount + conflict).await; 793 c.mine_conf().await; 794 c.worker().await; 795 796 step("Conflict bounce"); 797 let b = c.w_balance().await; 798 let amount = Amount::from_sat(20000); 799 // Perform bounce 800 c.malformed_credit(amount).await; 801 c.mine_pending().await; 802 c.expect_w_balance(b + amount).await; 803 c.mine_conf().await; 804 c.worker().await; 805 c.expect_w_balance(b + fee).await; 806 // Abandon pending transaction 807 c.restart_node(&["-minrelaytxfee=0.0002"]).await; 808 c.abandon_wire().await; 809 c.expect_w_balance(b + amount).await; 810 // Generate conflict 811 c.malformed_credit(amount).await; 812 c.mine_conf().await; 813 c.worker().await; 814 c.expect_w_balance(b + amount + fee).await; 815 // Handle conflict 816 c.mine_pending().await; 817 c.worker().await; 818 c.expect_w_balance(b + fee * 2).await; 819 c.mine_conf().await; 820 c.worker().await; 821 822 step("Reorg withdrawal"); 823 c.cluster_deco().await; 824 let before = c.w_balance().await; 825 c.withdrawal(Amount::from_sat(100000)).await; 826 c.mine_conf().await; 827 let after = c.w_balance().await; 828 c.worker().await; 829 assert!(c.status); 830 let fork = c.cluster_fork().await; 831 c.worker().await; 832 assert!(!c.status); 833 c.expect_w_balance(before).await; 834 c.mine(fork).await; 835 c.worker().await; 836 assert!(c.status); 837 c.expect_w_balance(after).await; 838 839 step("Reorg deposit"); 840 c.cluster_deco().await; 841 let before = c.c_balance().await; 842 c.transfer(Amount::from_sat(1111)).await; 843 c.worker().await; 844 c.mine_conf().await; 845 c.worker().await; 846 let after = c.c_balance().await; 847 assert!(c.status); 848 let fork = c.cluster_fork().await; 849 c.worker().await; 850 assert!(c.status); 851 c.expect_c_balance(before).await; 852 c.mine(fork).await; 853 c.worker().await; 854 assert!(c.status); 855 c.expect_c_balance(after).await; 856 857 step("Reorg bounce"); 858 c.cluster_deco().await; 859 c.malformed_credit(Amount::from_sat(100000)).await; 860 c.mine_conf().await; 861 c.worker().await; 862 c.mine_pending().await; 863 assert!(c.status); 864 let fork = c.cluster_fork().await; 865 c.worker().await; 866 assert!(!c.status); 867 c.mine(fork).await; 868 c.worker().await; 869 assert!(c.status); 870 871 c.s.bump_delay = Some(1); 872 873 step("Bump fee"); 874 let amount = Amount::from_sat(40000); 875 let b = c.c_balance().await; 876 c.transfer(amount).await; 877 c.worker().await; 878 let until = tokio::time::Instant::now() + Duration::from_secs(1); 879 c.worker().await; 880 c.restart_node(&["-minrelaytxfee=0.0003"]).await; 881 tokio::time::sleep_until(until).await; 882 c.worker().await; 883 c.expect_c_balance(b).await; 884 c.mine_conf().await; 885 c.expect_c_balance(b + amount).await; 886 c.worker().await; 887 888 step("Bump fee failure"); 889 let amount = Amount::from_sat(41000); 890 let b = c.c_balance().await; 891 c.transfer(amount).await; 892 c.worker().await; 893 let until = tokio::time::Instant::now() + Duration::from_secs(1); 894 c.worker().await; 895 c.restart_node(&["-minrelaytxfee=0.0004"]).await; 896 tokio::time::sleep_until(until).await; 897 set_failure_scenario(&["bumpfee"]); 898 c.w_injected_err().await; 899 c.worker().await; 900 c.expect_c_balance(b).await; 901 c.mine_conf().await; 902 c.expect_c_balance(b + amount).await; 903 c.worker().await; 904 905 step("Bump fee reorg"); 906 c.cluster_deco().await; 907 let amount = Amount::from_sat(42000); 908 let b = c.c_balance().await; 909 c.transfer(amount).await; 910 c.worker().await; 911 let until = tokio::time::Instant::now() + Duration::from_secs(1); 912 c.worker().await; 913 c.cluster_fork().await; 914 c.restart_node(&["-minrelaytxfee=0.0005"]).await; 915 c.next_block().await; 916 tokio::time::sleep_until(until).await; 917 c.worker().await; 918 c.expect_c_balance(b).await; 919 c.mine_conf().await; 920 c.expect_c_balance(b + amount).await; 921 c.worker().await; 922 923 c.s.bump_delay = None; 924 925 step("Reorg conflict withdrawal"); 926 let b = c.w_balance().await; 927 c.cluster_deco().await; 928 929 // Withdrawal 930 let amount = Amount::from_sat(420000); 931 let (id, _) = c.withdrawal(amount).await; 932 c.mine_conf().await; 933 c.worker().await; 934 c.expect_w_balance(b + amount).await; 935 936 // Perform fork 937 assert!(c.status); 938 c.cluster_fork().await; 939 c.worker().await; 940 assert!(!c.status); 941 942 // Generate conflict 943 c.restart_node(&["-minrelaytxfee=0.0006"]).await; 944 c.abandon_client().await; 945 let conflict = Amount::from_sat(54000); 946 c.withdrawal(conflict).await; 947 c.mine_pending().await; 948 c.expect_w_balance(b + conflict).await; 949 950 // Check cannot recover again adversarial attack 951 c.mine_conf().await; 952 c.worker().await; 953 assert!(!c.status); 954 955 // Manual drop from db 956 c.forget_tx(&id).await; 957 c.worker().await; 958 assert!(c.status); 959 c.expect_w_balance(b + conflict).await; 960 961 step("Reorg conflict bounce"); 962 let b = c.w_balance().await; 963 c.cluster_deco().await; 964 965 // Withdrawal 966 let amount = Amount::from_sat(420000); 967 let id = c.malformed_credit(amount).await; 968 c.mine_conf().await; 969 c.worker().await; 970 c.expect_w_balance(b + fee).await; 971 972 // Perform fork 973 assert!(c.status); 974 c.cluster_fork().await; 975 c.worker().await; 976 assert!(!c.status); 977 978 // Generate conflict 979 c.restart_node(&["-minrelaytxfee=0.0007"]).await; 980 c.abandon_client().await; 981 let conflict = Amount::from_sat(54000); 982 c.withdrawal(conflict).await; 983 c.mine_pending().await; 984 c.expect_w_balance(b + conflict).await; 985 986 // Check cannot recover again adversarial attack 987 c.mine_conf().await; 988 c.worker().await; 989 assert!(!c.status); 990 991 // Manual drop from db 992 c.forget_tx(&id).await; 993 c.worker().await; 994 assert!(c.status); 995 c.expect_w_balance(b + conflict).await; 996 997 step("Analysis"); 998 assert_eq!(c.s.conf, 3); 999 c.s.conf = analysis(&mut c.rpc, c.s.conf, c.s.max_conf).await?; 1000 assert_eq!(c.s.conf, 6); 1001 1002 step("Maxfee"); 1003 c.restart_node(&["-maxtxfee=0.0000001", "-minrelaytxfee=0.0000001"]) 1004 .await; 1005 let amount = Amount::from_sat(34500); 1006 let b = c.c_balance().await; 1007 c.transfer(amount).await; 1008 c.w_rpc_err().await; 1009 c.mine_pending().await; 1010 c.w_rpc_err().await; 1011 c.expect_c_balance(b).await; 1012 c.restart_node(&[]).await; 1013 c.worker().await; 1014 c.mine_conf().await; 1015 c.expect_c_balance(b + amount).await; 1016 c.worker().await; 1017 1018 c.abandon_client().await; 1019 c.abandon_wire().await; 1020 1021 step("Stop bitcoin network"); 1022 let cb = c.c_balance().await; 1023 let wb = c.w_balance().await; 1024 c.worker().await; 1025 c.mine_conf().await; 1026 c.worker().await; 1027 c.expect_c_balance(cb).await; 1028 c.expect_w_balance(wb).await; 1029 1030 c.rpc.stop().await?; 1031 c.rpc2.stop().await?; 1032 1033 n1.0.wait()?; 1034 n2.0.wait()?; 1035 1036 step("Config"); 1037 // Connect with custom cookie files 1038 sc.test_cfg( 1039 " 1040 rpccookiefile=catch_me_if_you_can 1041 ", 1042 &format!( 1043 " 1044 [depolymerizer-bitcoin-worker] 1045 RPC_COOKIE_FILE={}/regtest/catch_me_if_you_can 1046 ", 1047 sc.c_dir 1048 ), 1049 ) 1050 .await; 1051 // Connect with password 1052 sc.test_cfg( 1053 " 1054 rpcuser=bob 1055 rpcpassword=password 1056 ", 1057 " 1058 [depolymerizer-bitcoin-worker] 1059 RPC_AUTH_METHOD=basic 1060 RPC_USERNAME=bob 1061 RPC_PASSWORD=password 1062 ", 1063 ) 1064 .await; 1065 // Connect with token 1066 sc.test_cfg( 1067 " 1068 rpcauth=bob:9641cec731e1fad1ded02e1d31536e44$36b8b8af0a38104997a57f017805ff56bf8963ae4a2ed40252ca0e0e070fc19e 1069 ", 1070 " 1071 [depolymerizer-bitcoin-worker] 1072 RPC_AUTH_METHOD=basic 1073 RPC_USERNAME=bob 1074 RPC_PASSWORD=password 1075 ", 1076 ) 1077 .await; 1078 Ok(()) 1079 } 1080 1081 #[derive(clap::Parser, Debug)] 1082 #[command(no_binary_name = true)] 1083 struct Shell { 1084 #[clap(subcommand)] 1085 cmd: Option<Cmd>, 1086 } 1087 #[derive(Clone, clap::Subcommand, Debug)] 1088 enum Cmd { 1089 Setup, 1090 Reset, 1091 ResetDb, 1092 Sync, 1093 Credit, 1094 Debit, 1095 Mine { 1096 amount: Option<u32>, 1097 addr: Option<Address<NetworkUnchecked>>, 1098 }, 1099 Exit, 1100 Tx { 1101 txid: Txid, 1102 }, 1103 Track { 1104 txid: Txid, 1105 }, 1106 Untrack { 1107 txid: Txid, 1108 }, 1109 } 1110 1111 async fn online_harness(network: Network, reset: bool) -> PanicRes<()> { 1112 // Prepare dir 1113 let network_dir = match network { 1114 Network::Local => "btc-local", 1115 Network::Test => "btc-test", 1116 }; 1117 let dir = format!("depolymerizer-bitcoin/test/{network_dir}"); 1118 std::fs::create_dir_all(&dir)?; 1119 if reset { 1120 std::fs::remove_dir_all(&dir)?; 1121 } 1122 1123 // Bitcoin config 1124 let cfg = match network { 1125 Network::Local => { 1126 " 1127 chain=regtest 1128 txindex=1 1129 rpcservertimeout=0 1130 fallbackfee=0.00000001 1131 [regtest] 1132 rpcport=18345 1133 " 1134 } 1135 Network::Test => { 1136 " 1137 chain=signet 1138 txindex=1 1139 rpcservertimeout=0 1140 fallbackfee=0.00000001 1141 [signet] 1142 rpcport=18345 1143 " 1144 } 1145 }; 1146 std::fs::create_dir_all(&dir)?; 1147 std::fs::write(format!("{dir}/bitcoin.conf"), cfg)?; 1148 1149 let mut n = cmd("bitcoind", &[format!("-datadir={dir}").as_str()]); 1150 let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 18345).into(); 1151 1152 let datadir = match network { 1153 Network::Local => format!("{dir}/regtest"), 1154 Network::Test => format!("{dir}/signet"), 1155 }; 1156 let cookie = format!("{datadir}/.cookie"); 1157 let mut rpc = wait_up(&RpcCfg { 1158 addr, 1159 auth: RpcAuth::Cookie(cookie.clone()), 1160 }) 1161 .await; 1162 1163 for name in ["wire", "client"] { 1164 if let Err(e) = rpc.load_wallet(name).await { 1165 if let Error::RPC { 1166 code: ErrorCode::RpcWalletNotFound, 1167 .. 1168 } = e 1169 { 1170 rpc.create_wallet(name, "").await?; 1171 } else { 1172 break; 1173 } 1174 } 1175 } 1176 1177 let wire_addr = &rpc.wallet("wire").gen_addr().await?; 1178 let client_addr = &rpc.wallet("client").gen_addr().await?; 1179 1180 let cfg = Config::from_mem_with_env( 1181 CONFIG_SOURCE, 1182 &format!( 1183 " 1184 [depolymerizer-bitcoin] 1185 CURRENCY = BTC 1186 NAME = Exchange Owner 1187 WALLET = {wire_addr} 1188 1189 [depolymerizer-bitcoin-worker] 1190 BOUNCE_FEE = BTC:0.00001 1191 CONFIRMATION = 1 1192 RPC_BIND = {addr} 1193 WALLET_NAME = wire 1194 RPC_COOKIE_FILE = {cookie} 1195 " 1196 ), 1197 ) 1198 .unwrap(); 1199 1200 step("Setup"); 1201 let mut pool = dbinit(&cfg, reset).await?; 1202 setup(&cfg, reset).await?; 1203 let mut s = WorkerCfg::parse(&cfg)?; 1204 let mut status = true; 1205 1206 let mut repl: Repl<Shell> = Repl::new(".btc_history"); 1207 let mut tracked = BTreeSet::new(); 1208 loop { 1209 let info = rpc.get_blockchain_info().await.unwrap(); 1210 let wire_balance = rpc.wallet("wire").get_balance().await.unwrap(); 1211 println!("wire {wire_addr} {wire_balance}"); 1212 let client_balance = rpc.wallet("client").get_balance().await.unwrap(); 1213 println!("client {client_addr} {client_balance}"); 1214 for txid in &tracked { 1215 match rpc.wallet("client").get_tx(txid).await { 1216 Ok(info) => println!( 1217 "{} {txid} {} {}", 1218 "tx".cyan(), 1219 info.amount, 1220 info.confirmations 1221 ), 1222 Err(e) => println!("{} {txid} {}", "tx".cyan(), e.red()), 1223 } 1224 } 1225 if let Some(shell) = 1226 repl.read_line(&info.chain, &format!("{:.6}", info.verification_progress)) 1227 { 1228 let Some(cmd) = shell.cmd else { continue }; 1229 match cmd { 1230 Cmd::Setup => setup(&cfg, false).await?, 1231 Cmd::Reset => { 1232 pool = dbinit(&cfg, true).await?; 1233 } 1234 Cmd::ResetDb => { 1235 pool = dbinit(&cfg, true).await?; 1236 setup(&cfg, false).await?; 1237 } 1238 Cmd::Sync => { 1239 let db = &mut pool.acquire().await.unwrap().detach(); 1240 let res = worker_step(db, &mut rpc.wallet("wire"), &mut s, &mut status).await; 1241 if let Err(e) = &res { 1242 tracing::error!(target: "worker", "{e}"); 1243 } 1244 } 1245 Cmd::Credit => { 1246 let reserve_pub = EddsaPublicKey::rand(); 1247 let amount = amount("DEVBTC:0.00012"); 1248 match rpc 1249 .wallet("client") 1250 .send_segwit_key(wire_addr, taler_to_btc(&amount), &reserve_pub) 1251 .await 1252 { 1253 Ok(txid) => { 1254 tracked.insert(txid); 1255 info!(target: "testbench", "Credit {reserve_pub} {amount} {txid} to {wire_addr}"); 1256 } 1257 Err(e) => tracing::error!(target: "worker", "{e}"), 1258 } 1259 } 1260 Cmd::Debit => { 1261 let payto = FullBtcPayto::new(BtcWallet(client_addr.clone()), "Bitcoin Client"); 1262 let wtid = ShortHashCode::rand(); 1263 let amount = amount("DEVBTC:0.00011"); 1264 match db::transfer( 1265 &pool, 1266 &payto, 1267 &TransferRequest { 1268 request_uid: HashCode::rand(), 1269 amount, 1270 exchange_base_url: url("https://test.com"), 1271 metadata: None, 1272 wtid, 1273 credit_account: payto.as_uri(), 1274 }, 1275 ) 1276 .await 1277 .unwrap() 1278 { 1279 db::TransferResult::Success(_) => {} 1280 it => panic!("{it:?}"), 1281 }; 1282 info!(target: "testbench", "Debit {wtid} {amount} to {wire_addr}"); 1283 } 1284 Cmd::Mine { amount, addr } => { 1285 let amount = amount.unwrap_or(1); 1286 let addr = addr.map(|a| a.assume_checked()); 1287 let addr = addr.as_ref().unwrap_or(client_addr); 1288 rpc.wallet("client").mine(amount, addr).await.unwrap(); 1289 } 1290 Cmd::Exit => break, 1291 Cmd::Tx { txid } => { 1292 let info = rpc.wallet("client").get_tx(&txid).await.unwrap(); 1293 info!(target: "testbench", "{txid} {} {}", info.amount, info.confirmations); 1294 } 1295 Cmd::Track { txid } => { 1296 tracked.insert(txid); 1297 } 1298 Cmd::Untrack { txid } => { 1299 tracked.retain(|id| *id != txid); 1300 } 1301 } 1302 } else { 1303 break; 1304 } 1305 } 1306 1307 step("Finish"); 1308 rpc.stop().await?; 1309 n.0.wait()?; 1310 1311 Ok(()) 1312 } 1313 1314 fn main() { 1315 let args = Args::parse(); 1316 taler_main(CONFIG_SOURCE, args.common, async |_| { 1317 match args.cmd { 1318 Command::Logic { reset } => logic_harness(reset).await.unwrap(), 1319 Command::Online { reset, network } => online_harness(network, reset).await.unwrap(), 1320 } 1321 Ok(()) 1322 }); 1323 }