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