depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

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 }