/* This file is part of TALER Copyright (C) 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ use std::{ ops::{Deref, DerefMut}, path::{Path, PathBuf}, str::FromStr, thread::sleep, time::Duration, }; use bitcoin::{hashes::Hash, Address, Amount, BlockHash, Network, SignedAmount, Txid}; use btc_wire::{ btc_config::BitcoinConfig, rpc::{self, Category, ErrorCode, Rpc}, rpc_utils::{self, segwit_min_amount}, taler_utils::{btc_payto_url, btc_to_taler}, WireState, }; use common::{currency::CurrencyBtc, metadata::OutMetadata, postgres::NoTls, rand_slice}; use tempfile::TempDir; use crate::utils::{ check_incoming, check_outgoing, cmd_redirect, cmd_redirect_ok, print_now, retry, retry_opt, transfer, unused_port, ChildGuard, TalerCtx, TestCtx, }; pub const CLIENT: &str = "client"; pub const WIRE: &str = "wire"; fn unsigned(amount: SignedAmount) -> Amount { amount.abs().to_unsigned().unwrap() } fn wait_for_pending(since: &mut BlockHash, client_rpc: &mut Rpc, wire_rpc: &mut Rpc) { print_now("Wait for pending transactions mining:"); 'l: loop { std::thread::sleep(Duration::from_secs(1)); // Wait for btc-wire to act let sync = client_rpc.list_since_block(Some(since), 1).unwrap(); let sync2 = wire_rpc.list_since_block(Some(since), 1).unwrap(); *since = sync.lastblock; for tx in sync .transactions .into_iter() .chain(sync2.transactions.into_iter()) { if tx.confirmations == 0 { client_rpc.wait_for_new_block().unwrap(); print_now("."); continue 'l; } } break; } println!(); } pub fn online_test(config: Option<&Path>, base_url: &str) { let state = WireState::load_taler_config(config); if state.btc_config.network == Network::Bitcoin { panic!("You should never run this test on a real bitcoin network"); } let mut rpc = Rpc::common(&state.btc_config).expect("Cannot connect to bitcoin node"); // Load client match rpc.load_wallet(CLIENT) { Ok(_) => {} Err(rpc::Error::RPC { code: ErrorCode::RpcWalletNotFound, .. }) => { rpc.create_wallet(CLIENT, "").unwrap(); } Err(rpc::Error::RPC { code: ErrorCode::RpcWalletError | ErrorCode::RpcWalletAlreadyLoaded, .. }) => {} Err(e) => panic!("{}", e), }; let mut client_rpc = Rpc::wallet(&state.btc_config, CLIENT).unwrap(); let client_addr = client_rpc.gen_addr().unwrap(); let min_fund = Amount::from_sat(10_000); if client_rpc.get_balance().unwrap() < min_fund { println!( "Client need a minimum of {} BTC to run this test, send coins to this address: {}", min_fund.to_btc(), client_addr ); print_now("Waiting for fund:"); while client_rpc.get_balance().unwrap() < min_fund { client_rpc.wait_for_new_block().unwrap(); print_now("."); } println!(); } let mut since = client_rpc.list_since_block(None, 1).unwrap().lastblock; // Load wire let mut wire_rpc = Rpc::wallet(&state.btc_config, WIRE).unwrap(); let wire_addr = wire_rpc.gen_addr().unwrap(); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); // Load balances let client_balance = client_rpc.get_balance().unwrap(); let wire_balance = wire_rpc.get_balance().unwrap(); // Test amount let test_amount = Amount::from_sat(2000); let min_send_amount = rpc_utils::segwit_min_amount(); // To small to send back let min_bounce_amount = min_send_amount + Amount::from_sat(999); // To small after bounce fee let taler_test_amount = btc_to_taler(&test_amount.to_signed().unwrap(), state.currency); println!("Send transaction"); let reserve_pub_key = rand_slice(); let credit_id = client_rpc .send_segwit_key(&wire_addr, &test_amount, &reserve_pub_key) .unwrap(); let bounce_min_id = client_rpc .send(&wire_addr, &min_bounce_amount, None, false) .unwrap(); let send_min_id = client_rpc .send(&wire_addr, &min_send_amount, None, false) .unwrap(); let bounce_id = client_rpc .send(&wire_addr, &test_amount, None, false) .unwrap(); let client_sent_amount_cost = test_amount + min_send_amount * 2 + min_bounce_amount + min_send_amount + test_amount; let client_sent_fees_cost: Amount = [credit_id, bounce_min_id, send_min_id, bounce_id] .into_iter() .map(|id| unsigned(client_rpc.get_tx(&id).unwrap().fee.unwrap())) .reduce(|acc, i| acc + i) .unwrap(); let new_balance = client_rpc.get_balance().unwrap(); assert_eq!( client_balance - client_sent_amount_cost - client_sent_fees_cost, new_balance ); print_now("Wait for bounce:"); 'l: loop { let sync = client_rpc.list_since_block(Some(&since), 1).unwrap(); for tx in sync.transactions { if tx.category == Category::Receive { let (_, metadata) = client_rpc.get_tx_op_return(&tx.txid).unwrap(); let metadata = OutMetadata::decode(&metadata).unwrap(); match metadata { OutMetadata::Debit { .. } => {} OutMetadata::Bounce { bounced } => { let bounced = Txid::from_byte_array(bounced); if bounced == bounce_id { break 'l; } else if bounced == send_min_id { panic!("Bounced send min"); } else if bounced == bounce_min_id { panic!("Bounced bounce min"); } } } } } since = sync.lastblock; rpc.wait_for_new_block().unwrap(); print_now("."); } println!(); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); println!("Check balance"); let new_wire_balance = wire_rpc.get_balance().unwrap(); let new_client_balance = client_rpc.get_balance().unwrap(); assert!(new_wire_balance > wire_balance + test_amount + min_bounce_amount + min_send_amount); assert!( new_wire_balance < wire_balance + test_amount * 2 + min_bounce_amount + min_send_amount ); println!("Check incoming history"); assert!(check_incoming( base_url, &[(reserve_pub_key, taler_test_amount.clone())] )); println!("Get back some money"); let wtid = rand_slice(); transfer( base_url, &wtid, &state.base_url, btc_payto_url(&client_addr), &taler_test_amount, ); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); println!("Check balances"); let last_client_balance = client_rpc.get_balance().unwrap(); assert_eq!(new_client_balance + test_amount, last_client_balance); println!("Check outgoing history"); assert!(check_outgoing( base_url, &state.base_url, &[(wtid, taler_test_amount)] )); } pub struct BtcCtx { btc_node: ChildGuard, _btc_node2: ChildGuard, common_rpc: Rpc, common_rpc2: Rpc, wire_rpc: Rpc, client_rpc: Rpc, reserve_rpc: Rpc, wire_addr: Address, pub client_addr: Address, reserve_addr: Address, state: WireState, conf: u16, ctx: TalerCtx, node2_addr: String, } impl Deref for BtcCtx { type Target = TalerCtx; fn deref(&self) -> &Self::Target { &self.ctx } } impl DerefMut for BtcCtx { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.ctx } } impl BtcCtx { pub fn config(test_name: &str, config: &str) { // Generate temporary dirs let ctx = TestCtx::new(test_name); // Bitcoin config let config = PathBuf::from_str("instrumentation/conf") .unwrap() .join(config); let wire_dir = TempDir::new().unwrap(); let wire_dir = wire_dir.path(); std::fs::copy(config, wire_dir.join("bitcoin.conf")).unwrap(); // Load config let config = BitcoinConfig::load(wire_dir.join("bitcoin.conf"), CurrencyBtc::Dev).unwrap(); // Start bitcoin nodes let _btc_node = cmd_redirect( "bitcoind", &[&format!("-datadir={}", wire_dir.to_string_lossy())], &ctx.log("bitcoind"), ); // Connect retry(|| { Rpc::common(&config) .ok() .and_then(|mut it| it.get_blockchain_info().ok()) .is_some() }) } fn patch_config(from: &str, to: PathBuf, port: u16, rpc_port: u16) { let mut config = ini::Ini::load_from_file(from).unwrap(); config .with_section(Some("regtest")) .set("port", port.to_string()) .set("rpcport", rpc_port.to_string()); config.write_to_file(to).unwrap(); } pub fn setup(ctx: &TestCtx, taler_config: &str, stressed: bool) -> Self { let mut ctx = TalerCtx::new(ctx, "btc-wire", taler_config, stressed); // Choose unused port let btc_port = unused_port(); let btc_rpc_port = unused_port(); let btc2_port = unused_port(); let btc2_rpc_port = unused_port(); // Bitcoin config Self::patch_config( "instrumentation/conf/bitcoin.conf", ctx.wire_dir.join("bitcoin.conf"), btc_port, btc_rpc_port, ); Self::patch_config( "instrumentation/conf/bitcoin2.conf", ctx.wire2_dir.join("bitcoin.conf"), btc2_port, btc2_rpc_port, ); // Load config let state = WireState::load_taler_config(Some(&ctx.conf)); let btc_config2 = BitcoinConfig::load(ctx.wire2_dir.join("bitcoin.conf"), state.currency).unwrap(); // Start bitcoin nodes let btc_node = cmd_redirect( "bitcoind", &[&format!("-datadir={}", ctx.wire_dir.to_string_lossy())], &ctx.log("bitcoind"), ); let _btc_node2 = cmd_redirect( "bitcoind", &[&format!("-datadir={}", ctx.wire2_dir.to_string_lossy())], &ctx.log("bitcoind2"), ); ctx.init_db(); // Generate wallet cmd_redirect_ok( "btc-wire", &["-c", ctx.conf.to_str().unwrap(), "initwallet"], &ctx.log("cmd"), "wire initwallet", ); ctx.run(); // Setup wallets let mut common_rpc = retry_opt(|| Rpc::common(&state.btc_config).ok()); let node2_addr = format!("127.0.0.1:{btc2_port}"); common_rpc.add_node(&node2_addr).unwrap(); for name in ["client", "reserve"] { common_rpc.create_wallet(name, "").unwrap(); } let common_rpc2 = retry_opt(|| Rpc::common(&btc_config2).ok()); // Generate money let mut reserve_rpc = Rpc::wallet(&state.btc_config, "reserve").unwrap(); let mut client_rpc = Rpc::wallet(&state.btc_config, "client").unwrap(); let mut wire_rpc = Rpc::wallet(&state.btc_config, "wire").unwrap(); let reserve_addr = reserve_rpc.gen_addr().unwrap(); let client_addr = client_rpc.gen_addr().unwrap(); let wire_addr = wire_rpc.gen_addr().unwrap(); common_rpc.mine(101, &reserve_addr).unwrap(); reserve_rpc .send(&client_addr, &(Amount::ONE_BTC * 10), None, false) .unwrap(); common_rpc.mine(1, &reserve_addr).unwrap(); Self { ctx, btc_node, common_rpc, wire_rpc, client_rpc, reserve_rpc, wire_addr, client_addr, reserve_addr, conf: state.confirmation as u16, state, _btc_node2, common_rpc2, node2_addr, } } pub fn reset_db(&mut self) { let hash: BlockHash = self.common_rpc.get_genesis().unwrap(); let mut db = self.ctx.taler_conf.db_config().connect(NoTls).unwrap(); let mut tx = db.transaction().unwrap(); // Clear transaction tables and reset state tx.batch_execute("DELETE FROM tx_in;DELETE FROM tx_out;DELETE FROM bounce;") .unwrap(); tx.execute( "UPDATE state SET value=$1 WHERE name='last_hash'", &[&hash.as_byte_array().as_slice()], ) .unwrap(); tx.commit().unwrap(); } pub fn stop_node(&mut self) { // We need to kill bitcoin gracefully to avoid corruption #[cfg(unix)] { cmd_redirect_ok( "kill", &[&self.btc_node.0.id().to_string()], "/dev/null", "fill btc node", ); self.btc_node.0.wait().unwrap(); } } pub fn cluster_deco(&mut self) { self.common_rpc.disconnect_node(&self.node2_addr).unwrap(); } pub fn cluster_fork(&mut self, length: u16) { self.common_rpc2.mine(length, &self.reserve_addr).unwrap(); self.common_rpc.add_node(&self.node2_addr).unwrap(); } pub fn restart_node(&mut self, additional_args: &[&str]) { self.stop_node(); self.resume_node(additional_args); } pub fn resume_node(&mut self, additional_args: &[&str]) { let datadir = format!("-datadir={}", self.ctx.wire_dir.to_string_lossy()); let mut args = vec![datadir.as_str()]; args.extend_from_slice(additional_args); self.btc_node = cmd_redirect("bitcoind", &args, &self.ctx.log("bitcoind")); self.common_rpc = retry_opt(|| Rpc::common(&self.state.btc_config).ok()); self.common_rpc.add_node(&self.node2_addr).unwrap(); for name in ["client", "reserve", "wire"] { self.common_rpc.load_wallet(name).ok(); } self.reserve_rpc = Rpc::wallet(&self.state.btc_config, "reserve").unwrap(); self.client_rpc = Rpc::wallet(&self.state.btc_config, "client").unwrap(); self.wire_rpc = Rpc::wallet(&self.state.btc_config, "wire").unwrap(); } /* ----- Transaction ------ */ pub fn credit(&mut self, amount: Amount, metadata: [u8; 32]) { self.client_rpc .send_segwit_key(&self.wire_addr, &amount, &metadata) .unwrap(); } pub fn debit(&mut self, amount: Amount, metadata: [u8; 32]) { transfer( &self.ctx.gateway_url, &metadata, &self.state.base_url, btc_payto_url(&self.client_addr), &btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), ) } pub fn malformed_credit(&mut self, amount: &Amount) { self.client_rpc .send(&self.wire_addr, amount, None, false) .unwrap(); } pub fn reset_wallet(&mut self) { let amount = self.wire_balance(); self.wire_rpc .send(&self.client_addr, &amount, None, true) .unwrap(); self.next_block(); } fn abandon(rpc: &mut Rpc) { let list = rpc.list_since_block(None, 1).unwrap(); for tx in list.transactions { if tx.category == Category::Send && tx.confirmations == 0 { rpc.abandon_tx(&tx.txid).unwrap(); } } } pub fn abandon_wire(&mut self) { Self::abandon(&mut self.wire_rpc); } pub fn abandon_client(&mut self) { Self::abandon(&mut self.client_rpc); } /* ----- Mining ----- */ fn mine(&mut self, nb: u16) { self.common_rpc.mine(nb, &self.reserve_addr).unwrap(); } pub fn next_conf(&mut self) { self.mine(self.conf) } pub fn next_block(&mut self) { self.mine(1) } /* ----- Balances ----- */ pub fn client_balance(&mut self) -> Amount { self.client_rpc.get_balance().unwrap() } pub fn wire_balance(&mut self) -> Amount { self.wire_rpc.get_balance().unwrap() } fn expect_balance(&mut self, balance: Amount, mine: bool, lambda: fn(&mut Self) -> Amount) { retry(|| { let check = balance == lambda(self); if !check && mine { self.next_block(); } check }); } pub fn expect_client_balance(&mut self, balance: Amount, mine: bool) { self.expect_balance(balance, mine, Self::client_balance) } pub fn expect_wire_balance(&mut self, balance: Amount, mine: bool) { self.expect_balance(balance, mine, Self::wire_balance) } /* ----- Wire Gateway ----- */ pub fn expect_credits(&self, txs: &[([u8; 32], Amount)]) { let txs: Vec<_> = txs .iter() .map(|(metadata, amount)| { ( *metadata, btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), ) }) .collect(); self.ctx.expect_credits(&txs) } pub fn expect_debits(&self, txs: &[([u8; 32], Amount)]) { let txs: Vec<_> = txs .iter() .map(|(metadata, amount)| { ( *metadata, btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), ) }) .collect(); self.ctx.expect_debits(&self.state.base_url, &txs) } } /// Test btc-wire correctly receive and send transactions on the blockchain pub fn wire(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", false); ctx.step("Credit"); { // Send transactions let mut balance = ctx.wire_balance(); let mut txs = Vec::new(); for n in 10..100 { let metadata = rand_slice(); let amount = Amount::from_sat(n * 1000); ctx.credit(amount, metadata); txs.push((metadata, amount)); balance += amount; ctx.next_block(); } ctx.next_conf(); ctx.expect_credits(&txs); ctx.expect_wire_balance(balance, true); }; ctx.step("Debit"); { let mut balance = ctx.client_balance(); let mut txs = Vec::new(); for n in 10..100 { let metadata = rand_slice(); let amount = Amount::from_sat(n * 100); balance += amount; ctx.debit(amount, metadata); txs.push((metadata, amount)); } ctx.next_block(); ctx.expect_debits(&txs); ctx.expect_client_balance(balance, true); } ctx.step("Bounce"); { ctx.reset_wallet(); // Send bad transactions let mut balance = ctx.wire_balance(); for n in 10..40 { ctx.malformed_credit(&Amount::from_sat(n * 1000)); balance += ctx.state.bounce_fee; } ctx.next_conf(); ctx.expect_wire_balance(balance, true); } } /// Check btc-wire and wire-gateway correctly stop when a lifetime limit is configured pub fn lifetime(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc_lifetime.conf", false); ctx.step("Check lifetime"); // Start up retry(|| ctx.wire_running() && ctx.gateway_running()); // Consume lifetime for _ in 0..=ctx.taler_conf.wire_lifetime().unwrap() { ctx.credit(segwit_min_amount(), rand_slice()); ctx.next_block(); } for _ in 0..=ctx.taler_conf.http_lifetime().unwrap() { ctx.debit(segwit_min_amount(), rand_slice()); ctx.next_block(); } // End down retry(|| !ctx.wire_running() && !ctx.gateway_running()); } /// Check the capacity of wire-gateway and btc-wire to recover from database and node loss pub fn reconnect(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", false); let mut credits = Vec::new(); let mut debits = Vec::new(); ctx.step("With DB"); { let metadata = rand_slice(); let amount = Amount::from_sat(42000); ctx.credit(amount, metadata); credits.push((metadata, amount)); ctx.next_block(); ctx.next_conf(); ctx.expect_credits(&credits); }; ctx.step("Without DB"); { ctx.stop_db(); ctx.malformed_credit(&Amount::from_sat(24000)); let metadata = rand_slice(); let amount = Amount::from_sat(40000); ctx.credit(amount, metadata); credits.push((metadata, amount)); ctx.stop_node(); ctx.expect_error(); } ctx.step("Reconnect DB"); { ctx.resume_db(); ctx.resume_node(&[]); let metadata = rand_slice(); let amount = Amount::from_sat(2000); ctx.debit(amount, metadata); debits.push((metadata, amount)); ctx.next_block(); sleep(Duration::from_secs(3)); ctx.next_block(); sleep(Duration::from_secs(3)); ctx.next_block(); ctx.expect_debits(&debits); ctx.expect_credits(&credits); } ctx.step("Recover DB"); { let balance = ctx.wire_balance(); ctx.reset_db(); ctx.next_block(); ctx.expect_debits(&debits); ctx.expect_credits(&credits); ctx.expect_wire_balance(balance, true); } } /// Test btc-wire ability to recover from errors in correctness critical paths and prevent concurrent sending pub fn stress(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", true); let mut credits = Vec::new(); let mut debits = Vec::new(); ctx.step("Credit"); { let mut balance = ctx.wire_balance(); for n in 10..30 { let metadata = rand_slice(); let amount = Amount::from_sat(n * 1000); ctx.credit(amount, metadata); credits.push((metadata, amount)); balance += amount; ctx.next_block(); } ctx.next_conf(); ctx.expect_credits(&credits); ctx.expect_wire_balance(balance, true); }; ctx.step("Debit"); { let mut balance = ctx.client_balance(); for n in 10..30 { let metadata = rand_slice(); let amount = Amount::from_sat(n * 100); balance += amount; ctx.debit(amount, metadata); debits.push((metadata, amount)); } ctx.next_block(); ctx.expect_debits(&debits); ctx.expect_client_balance(balance, true); } ctx.step("Bounce"); { ctx.reset_wallet(); let mut balance = ctx.wire_balance(); for n in 10..30 { ctx.malformed_credit(&Amount::from_sat(n * 1000)); balance += ctx.state.bounce_fee; } ctx.next_conf(); ctx.expect_wire_balance(balance, true); } ctx.step("Recover DB"); { let balance = ctx.wire_balance(); ctx.reset_db(); ctx.next_block(); ctx.expect_debits(&debits); ctx.expect_credits(&credits); ctx.expect_wire_balance(balance, true); } } /// Test btc-wire ability to handle conflicting outgoing transactions pub fn conflict(tctx: TestCtx) { tctx.step("Setup"); let mut ctx = BtcCtx::setup(&tctx, "taler_btc.conf", false); ctx.step("Conflict send"); { // Perform credit let amount = Amount::from_sat(4200000); ctx.credit(amount, rand_slice()); ctx.next_conf(); ctx.expect_wire_balance(amount, true); let client = ctx.client_balance(); let wire = ctx.wire_balance(); // Perform debit ctx.debit(Amount::from_sat(400000), rand_slice()); retry(|| ctx.wire_balance() < wire); // Abandon pending transaction ctx.restart_node(&["-minrelaytxfee=0.0001"]); ctx.abandon_wire(); ctx.expect_client_balance(client, false); ctx.expect_wire_balance(wire, false); // Generate conflict ctx.debit(Amount::from_sat(500000), rand_slice()); retry(|| ctx.wire_balance() < wire); // Resend conflicting transaction ctx.restart_node(&[]); ctx.next_block(); let wire = ctx.wire_balance(); retry(|| ctx.wire_balance() < wire); } ctx.step("Setup"); drop(ctx); let mut ctx = BtcCtx::setup(&tctx, "taler_btc.conf", false); ctx.credit(Amount::from_sat(3000000), rand_slice()); ctx.next_block(); ctx.step("Conflict bounce"); { // Perform bounce let wire = ctx.wire_balance(); let bounce_amount = Amount::from_sat(4000000); ctx.malformed_credit(&bounce_amount); ctx.next_conf(); let fee = ctx.state.bounce_fee; ctx.expect_wire_balance(wire + fee, true); // Abandon pending transaction ctx.restart_node(&["-minrelaytxfee=0.0001"]); ctx.abandon_wire(); ctx.expect_wire_balance(wire + bounce_amount, false); // Generate conflict let amount = Amount::from_sat(50000); ctx.debit(amount, rand_slice()); retry(|| ctx.wire_balance() < (wire + bounce_amount)); // Resend conflicting transaction ctx.restart_node(&[]); let wire = ctx.wire_balance(); ctx.next_block(); retry(|| ctx.wire_balance() < wire); } } /// Test btc-wire correctness when a blockchain reorganization occurs pub fn reorg(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", false); ctx.step("Handle reorg incoming transactions"); { // Loose second bitcoin node ctx.cluster_deco(); // Perform credits let before = ctx.wire_balance(); for n in 10..21 { ctx.credit(Amount::from_sat(n * 10000), rand_slice()); ctx.next_block(); } let after = ctx.wire_balance(); // Perform fork and check btc-wire hard error ctx.expect_gateway_up(); ctx.cluster_fork(22); ctx.expect_wire_balance(before, false); ctx.expect_gateway_down(); // Recover orphaned transaction ctx.mine(12); ctx.expect_wire_balance(after, false); ctx.expect_gateway_up(); } ctx.step("Handle reorg outgoing transactions"); { // Loose second bitcoin node ctx.cluster_deco(); // Perform debits let before = ctx.client_balance(); let mut after = ctx.client_balance(); for n in 10..21 { let amount = Amount::from_sat(n * 100); ctx.debit(amount, rand_slice()); after += amount; } ctx.next_block(); ctx.expect_client_balance(after, true); // Perform fork and check btc-wire still up ctx.expect_gateway_up(); ctx.cluster_fork(22); ctx.expect_client_balance(before, false); ctx.expect_gateway_up(); // Recover orphaned transaction ctx.next_conf(); ctx.expect_client_balance(after, false); } ctx.step("Handle reorg bounce"); { ctx.reset_wallet(); // Loose second bitcoin node ctx.cluster_deco(); // Perform bounce let before = ctx.wire_balance(); let mut after = ctx.wire_balance(); for n in 10..21 { ctx.malformed_credit(&Amount::from_sat(n * 1000)); after += ctx.state.bounce_fee; } ctx.next_conf(); ctx.expect_wire_balance(after, true); // Perform fork and check btc-wire hard error ctx.expect_gateway_up(); ctx.cluster_fork(22); ctx.expect_wire_balance(before, false); ctx.expect_gateway_down(); // Recover orphaned transaction ctx.mine(10); ctx.expect_wire_balance(after, false); ctx.expect_gateway_up(); } } /// Test btc-wire correctness when a blockchain reorganization occurs leading to past incoming transaction conflict pub fn hell(ctx: TestCtx) { fn step(ctx: &TestCtx, name: &str, action: impl FnOnce(&mut BtcCtx)) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(ctx, "taler_btc.conf", false); ctx.step(name); // Loose second bitcoin node ctx.cluster_deco(); // Perform action action(&mut ctx); // Perform fork and check btc-wire hard error ctx.expect_gateway_up(); ctx.cluster_fork(ctx.conf * 2); ctx.expect_gateway_down(); // Generate conflict ctx.restart_node(&["-minrelaytxfee=0.001"]); ctx.abandon_client(); let amount = Amount::from_sat(54000); ctx.credit(amount, rand_slice()); ctx.expect_wire_balance(amount, true); // Check btc-wire suspend operation let bounce_amount = Amount::from_sat(34000); ctx.malformed_credit(&bounce_amount); ctx.next_conf(); ctx.expect_wire_balance(amount + bounce_amount, true); ctx.expect_gateway_down(); } step(&ctx, "Handle reorg conflicting incoming credit", |ctx| { let amount = Amount::from_sat(420000); ctx.credit(amount, rand_slice()); ctx.next_conf(); ctx.expect_wire_balance(amount, true); }); step(&ctx, "Handle reorg conflicting incoming bounce", |ctx| { let amount = Amount::from_sat(420000); ctx.malformed_credit(&amount); ctx.next_conf(); let fee = ctx.state.bounce_fee; ctx.expect_wire_balance(fee, true); }); } /// Test btc-wire ability to learn and protect itself from blockchain behavior pub fn analysis(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", false); ctx.step("Learn from reorg"); // Loose second bitcoin node ctx.cluster_deco(); // Perform credit let before = ctx.wire_balance(); ctx.credit(Amount::from_sat(42000), rand_slice()); ctx.next_conf(); let after = ctx.wire_balance(); // Perform fork and check btc-wire hard error ctx.expect_gateway_up(); ctx.cluster_fork(5); ctx.expect_wire_balance(before, false); ctx.expect_gateway_down(); // Recover orphaned transaction ctx.next_conf(); ctx.next_block(); // Conf have changed ctx.expect_wire_balance(after, false); ctx.expect_gateway_up(); // Loose second bitcoin node ctx.cluster_deco(); // Perform credit let before = ctx.wire_balance(); ctx.credit(Amount::from_sat(42000), rand_slice()); ctx.next_conf(); // Perform fork and check btc-wire learned from previous attack ctx.expect_gateway_up(); ctx.cluster_fork(5); ctx.expect_wire_balance(before, false); std::thread::sleep(Duration::from_secs(3)); // Give some time for the gateway to be down ctx.expect_gateway_up(); } /// Test btc-wire ability to handle stuck transaction correctly pub fn bumpfee(tctx: TestCtx) { tctx.step("Setup"); let mut ctx = BtcCtx::setup(&tctx, "taler_btc_bump.conf", false); // Perform credits to allow wire to perform debits latter for n in 10..13 { ctx.credit(Amount::from_sat(n * 100000), rand_slice()); ctx.next_block(); } ctx.next_conf(); ctx.step("Bump fee"); { // Perform debit let mut client = ctx.client_balance(); let wire = ctx.wire_balance(); let amount = Amount::from_sat(40000); ctx.debit(amount, rand_slice()); retry(|| ctx.wire_balance() < wire); // Bump min relay fee making the previous debit stuck ctx.restart_node(&["-minrelaytxfee=0.0001"]); // Check bump happen client += amount; ctx.expect_client_balance(client, true); } ctx.step("Bump fee reorg"); { // Loose second bitcoin node ctx.cluster_deco(); // Perform debit let mut client = ctx.client_balance(); let wire = ctx.wire_balance(); let amount = Amount::from_sat(40000); ctx.debit(amount, rand_slice()); retry(|| ctx.wire_balance() < wire); // Bump min relay fee and fork making the previous debit stuck and problematic ctx.cluster_fork(6); ctx.restart_node(&["-minrelaytxfee=0.0001"]); // Check bump happen client += amount; ctx.expect_client_balance(client, true); } ctx.step("Setup"); drop(ctx); let mut ctx = BtcCtx::setup(&tctx, "taler_btc_bump.conf", true); // Perform credits to allow wire to perform debits latter for n in 10..61 { ctx.credit(Amount::from_sat(n * 100000), rand_slice()); ctx.next_block(); } ctx.next_conf(); ctx.step("Bump fee stress"); { // Loose second bitcoin node ctx.cluster_deco(); // Perform debits let client = ctx.client_balance(); let wire = ctx.wire_balance(); let mut total_amount = Amount::ZERO; for n in 10..31 { let amount = Amount::from_sat(n * 10000); total_amount += amount; ctx.debit(amount, rand_slice()); } retry(|| ctx.wire_balance() < wire - total_amount); // Bump min relay fee making the previous debits stuck ctx.restart_node(&["-minrelaytxfee=0.0001"]); // Check bump happen ctx.expect_client_balance(client + total_amount, true); } } /// Test btc-wire handle transaction fees exceeding limits pub fn maxfee(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = BtcCtx::setup(&ctx, "taler_btc.conf", false); // Perform credits to allow wire to perform debits latter for n in 10..31 { ctx.credit(Amount::from_sat(n * 100000), rand_slice()); ctx.next_block(); } ctx.next_conf(); let client = ctx.client_balance(); let wire = ctx.wire_balance(); let mut total_amount = Amount::ZERO; ctx.step("Too high fee"); { // Change fee config ctx.restart_node(&["-maxtxfee=0.0000001", "-minrelaytxfee=0.0000001"]); // Perform debits for n in 10..31 { let amount = Amount::from_sat(n * 10000); total_amount += amount; ctx.debit(amount, rand_slice()); } sleep(Duration::from_secs(3)); // Check no transaction happen ctx.expect_wire_balance(wire, true); ctx.expect_client_balance(client, true); } ctx.step("Good feed"); { // Restore default config ctx.restart_node(&[]); // Check transaction now have been made ctx.expect_client_balance(client + total_amount, true); } } /// Test btc-wire ability to configure itself from bitcoin configuration pub fn config(ctx: TestCtx) { for n in 0..5 { let config_name = format!("bitcoin_auth{}.conf", n); ctx.step(format!("Config {}", config_name)); BtcCtx::config(&format!("config/{config_name}"), &config_name); } }