depolymerization

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

btc-harness.rs (37445B)


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