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