depolymerization

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

btc-harness.rs (37398B)


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