depolymerization

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

commit ec1d6043820b5a5d37e46fe2d26f228528663d60
parent 87a6363582cf58e8fbefaeb0fdff59ee068237af
Author: Antoine A <>
Date:   Thu, 24 Feb 2022 13:59:39 +0100

instrumentation: readme and better code

Diffstat:
Mbtc-wire/src/lib.rs | 38+++++++++++++++++++++++++++++++++++---
Mbtc-wire/src/main.rs | 42++++++++++--------------------------------
Ainstrumentation/README.md | 20++++++++++++++++++++
Ainstrumentation/src/btc.rs | 246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minstrumentation/src/main.rs | 254++-----------------------------------------------------------------------------
5 files changed, 316 insertions(+), 284 deletions(-)

diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs @@ -14,14 +14,16 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ use std::str::FromStr; +use std::sync::atomic::AtomicU16; use bitcoin::{hashes::hex::FromHex, Address, Amount, Network, Txid}; +use common::api_common::Amount as TalerAmount; use common::config::BtcConfig; +use config::BitcoinConfig; use rpc::{Category, Rpc, Transaction}; -use rpc_utils::{segwit_min_amount, sender_address}; +use rpc_utils::{default_data_dir, segwit_min_amount, sender_address}; use segwit::{decode_segwit_msg, encode_segwit_key}; use taler_utils::taler_to_btc; -use common::api_common::Amount as TalerAmount; pub mod config; pub mod metadata; @@ -140,11 +142,41 @@ impl Rpc { } const DEFAULT_BOUNCE_FEE: &'static str = "BTC:0.00001"; +const DEFAULT_CONFIRMATION: u16 = 6; -pub fn config_bounce_fee(config: &BtcConfig) -> Amount { +fn config_bounce_fee(config: &BtcConfig) -> Amount { let config = config.bounce_fee.as_deref().unwrap_or(DEFAULT_BOUNCE_FEE); TalerAmount::from_str(&config) .ok() .and_then(|a| taler_to_btc(&a).ok()) .expect("config value BOUNCE_FEE is no a valid bitcoin amount") } + +pub struct WireState { + pub confirmation: AtomicU16, + pub max_confirmation: u16, + pub config: BtcConfig, + pub btc_config: BitcoinConfig, + pub bounce_fee: Amount, +} + +impl WireState { + pub fn from_taler_config(config: BtcConfig) -> Self { + let data_dir = config + .core + .data_dir + .as_ref() + .cloned() + .unwrap_or_else(default_data_dir); + let btc_config = + BitcoinConfig::load(&data_dir).expect("Failed to read bitcoin configuration file"); + let init_confirmation = config.confirmation.unwrap_or(DEFAULT_CONFIRMATION); + Self { + confirmation: AtomicU16::new(init_confirmation), + max_confirmation: init_confirmation * 2, + bounce_fee: config_bounce_fee(&config), + config, + btc_config, + } + } +} diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs @@ -13,22 +13,22 @@ You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use bitcoin::{Amount as BtcAmount, Network}; +use bitcoin::Network; use btc_wire::{ config::{BitcoinConfig, WIRE_WALLET_NAME}, - config_bounce_fee, rpc::{self, auto_rpc_common, auto_rpc_wallet, ErrorCode, Rpc}, rpc_utils::default_data_dir, + WireState, }; use clap::StructOpt; use common::{ - config::{load_btc_config, BtcConfig, Config, CoreConfig}, + config::{load_btc_config, Config, CoreConfig}, log::log::info, named_spawn, password, postgres::{Client, NoTls}, reconnect::auto_reconnect_db, }; -use std::{path::PathBuf, sync::atomic::AtomicU16}; +use std::path::PathBuf; use crate::loops::{analysis::analysis, watcher::watcher, worker::worker}; @@ -36,14 +36,6 @@ mod fail_point; mod loops; mod sql; -const DEFAULT_CONFIRMATION: u16 = 6; -pub struct WireState { - confirmation: AtomicU16, - max_confirmation: u16, - config: BtcConfig, - bounce_fee: BtcAmount, -} - /// Taler wire for bitcoincore #[derive(clap::Parser, Debug)] struct Args { @@ -168,22 +160,16 @@ fn init(config: Option<PathBuf>, init: Init) { fn run(config: Option<PathBuf>) { let config = load_btc_config(config.as_deref()); - let data_dir = config - .core - .data_dir - .as_ref() - .cloned() - .unwrap_or_else(default_data_dir); - let btc_config = BitcoinConfig::load(&data_dir).unwrap(); + let state: &'static _ = Box::leak(Box::new(WireState::from_taler_config(config))); #[cfg(feature = "fail")] - if btc_config.network == Network::Regtest { + if state.btc_config.network == Network::Regtest { common::log::log::warn!("Running with random failures"); } else { common::log::log::error!("Running with random failures is unsuitable for production"); std::process::exit(1); } - let chain_name = match btc_config.network { + let chain_name = match state.btc_config.network { Network::Bitcoin => "main", Network::Testnet => "test", Network::Signet => "signet", @@ -191,17 +177,9 @@ fn run(config: Option<PathBuf>) { }; info!("Running on {} chain", chain_name); - let init_confirmation = config.confirmation.unwrap_or(DEFAULT_CONFIRMATION); - let state: &'static WireState = Box::leak(Box::new(WireState { - confirmation: AtomicU16::new(init_confirmation), - max_confirmation: init_confirmation * 2, - bounce_fee: config_bounce_fee(&config), - config, - })); - - let rpc_watcher = auto_rpc_common(btc_config.clone()); - let rpc_analysis = auto_rpc_common(btc_config.clone()); - let rpc_worker = auto_rpc_wallet(btc_config, WIRE_WALLET_NAME); + let rpc_watcher = auto_rpc_common(state.btc_config.clone()); + let rpc_analysis = auto_rpc_common(state.btc_config.clone()); + let rpc_worker = auto_rpc_wallet(state.btc_config.clone(), WIRE_WALLET_NAME); let db_watcher = auto_reconnect_db(state.config.core.db_url.clone()); let db_analysis = auto_reconnect_db(state.config.core.db_url.clone()); diff --git a/instrumentation/README.md b/instrumentation/README.md @@ -0,0 +1,20 @@ +# Depolymerizer instrumentation test + +Unit tests running on a private development network are meant to test the good +behavior in case of extreme situation but do not attest our capacity to handle +real network behavior. + +## Install + +`cargo install --path instrumentation` + +## Run + +First, follow a normal setup for the adapter and then run `instrumentation`. The +tested blockchain will be determined based on the taler configuration. + +## Temporary database + +If you do not want to use a persistent database for instrumentation tests, there +is a [script](../script/tmp_db.sh) to generate a temporary database similar to +unit tests. diff --git a/instrumentation/src/btc.rs b/instrumentation/src/btc.rs @@ -0,0 +1,246 @@ +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::{path::Path, time::Duration}; + +use bitcoin::{Amount, BlockHash, Network, SignedAmount}; +use btc_wire::{ + metadata::OutMetadata, + rpc::{self, Category, ErrorCode, Rpc, Transaction}, + rpc_utils, + taler_utils::{btc_payto_url, btc_to_taler}, + WireState, +}; +use common::{ + api_common::Base32, + api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, + config::load_btc_config, + rand_slice, +}; + +use crate::print_now; + +pub const CLIENT: &str = "client"; +pub const WIRE: &str = "wire"; + +fn unsigned(amount: SignedAmount) -> Amount { + amount.abs().to_unsigned().unwrap() +} + +fn wait_for_pending(since: &mut BlockHash, client_rpc: &mut Rpc, wire_rpc: &mut Rpc) { + print_now("Wait for pending transactions mining:"); + 'l: loop { + std::thread::sleep(Duration::from_secs(1)); // Wait for btc-wire to act + let sync = client_rpc.list_since_block(Some(&since), 1).unwrap(); + let sync2 = wire_rpc.list_since_block(Some(&since), 1).unwrap(); + *since = sync.lastblock; + for tx in sync + .transactions + .into_iter() + .chain(sync2.transactions.into_iter()) + { + if tx.confirmations == 0 { + client_rpc.wait_for_new_block().unwrap(); + print_now("."); + continue 'l; + } + } + break; + } + println!(""); +} + +pub fn btc_test(config: Option<&Path>, base_url: &str) { + let config = load_btc_config(config); + let state = WireState::from_taler_config(config); + + if state.btc_config.network == Network::Bitcoin { + panic!("You should never run this test on a real bitcoin network"); + } + + let min_fund = Amount::from_sat(10_000); + + let mut rpc = Rpc::common(&state.btc_config).unwrap(); + // Load client + match rpc.load_wallet(CLIENT) { + Ok(_) => {} + Err(rpc::Error::RPC { + code: ErrorCode::RpcWalletNotFound, + .. + }) => { + rpc.create_wallet(CLIENT, "").unwrap(); + } + Err(rpc::Error::RPC { + code: ErrorCode::RpcWalletError | ErrorCode::RpcWalletAlreadyLoaded, + .. + }) => {} + Err(e) => panic!("{}", e), + }; + let mut client_rpc = Rpc::wallet(&state.btc_config, CLIENT).unwrap(); + let client_addr = client_rpc.gen_addr().unwrap(); + if client_rpc.get_balance().unwrap() < min_fund { + println!( + "Client need a minimum of {} BTC to run this test, send coins to this address: {}", + min_fund.as_btc(), + client_addr + ); + print_now("Waiting for fund:"); + while client_rpc.get_balance().unwrap() < min_fund { + client_rpc.wait_for_new_block().unwrap(); + print_now("."); + } + println!(""); + } + let mut since = client_rpc.list_since_block(None, 1).unwrap().lastblock; + // Load wire + let mut wire_rpc = Rpc::wallet(&state.btc_config, WIRE).unwrap(); + let wire_addr = wire_rpc.gen_addr().unwrap(); + + wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); + + // Load balances + let client_balance = client_rpc.get_balance().unwrap(); + let wire_balance = wire_rpc.get_balance().unwrap(); + // Test amount + let test_amount = Amount::from_sat(2000); + let min_send_amount = rpc_utils::segwit_min_amount(); // To small to send back + let min_bounce_amount = min_send_amount + Amount::from_sat(999); // To small after bounce fee + let taler_test_amount = btc_to_taler(&test_amount.to_signed().unwrap()); + + println!("Send transaction"); + let reserve_pub_key = rand_slice(); + let deposit_id = client_rpc + .send_segwit_key(&wire_addr, &test_amount, &reserve_pub_key) + .unwrap(); + let bounce_min_id = client_rpc + .send(&wire_addr, &min_bounce_amount, None, false) + .unwrap(); + let send_min_id = client_rpc + .send(&wire_addr, &min_send_amount, None, false) + .unwrap(); + let bounce_id = client_rpc + .send(&wire_addr, &test_amount, None, false) + .unwrap(); + let client_sent_amount_cost = + test_amount + min_send_amount * 2 + min_bounce_amount + min_send_amount + test_amount; + let client_sent_fees_cost: Amount = [deposit_id, bounce_min_id, send_min_id, bounce_id] + .into_iter() + .map(|id| unsigned(client_rpc.get_tx(&id).unwrap().fee.unwrap())) + .reduce(|acc, i| acc + i) + .unwrap(); + let new_balance = client_rpc.get_balance().unwrap(); + assert_eq!( + client_balance - client_sent_amount_cost - client_sent_fees_cost, + new_balance + ); + + print_now("Wait for bounce:"); + let bounce: Transaction = 'l: loop { + let sync = client_rpc.list_since_block(Some(&since), 1).unwrap(); + for tx in sync.transactions { + if tx.category == Category::Receive { + let (tx, metadata) = client_rpc.get_tx_op_return(&tx.txid).unwrap(); + let metadata = OutMetadata::decode(&metadata).unwrap(); + match metadata { + OutMetadata::Withdraw { .. } => {} + OutMetadata::Bounce { bounced } => { + if bounced == bounce_id { + break 'l tx; + } else if bounced == send_min_id { + panic!("Bounced send min"); + } else if bounced == bounce_min_id { + panic!("Bounced bounce min"); + } + } + } + } + } + since = sync.lastblock; + rpc.wait_for_new_block().unwrap(); + print_now("."); + }; + println!(""); + wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); + + println!("Check balance"); + let new_client_balance = client_rpc.get_balance().unwrap(); + let new_wire_balance = wire_rpc.get_balance().unwrap(); + assert_eq!( + client_balance - client_sent_amount_cost - client_sent_fees_cost + unsigned(bounce.amount), + new_client_balance + ); + assert_eq!( + wire_balance + test_amount + min_bounce_amount + min_send_amount + state.bounce_fee, + new_wire_balance + ); + + println!("Check history"); + let history: IncomingHistory = ureq::get(&format!("{}/history/incoming", base_url)) + .query("delta", "-5") + .call() + .unwrap() + .into_json() + .unwrap(); + assert!(history + .incoming_transactions + .iter() + .find(|h| { + matches!( + h, + IncomingBankTransaction::IncomingReserveTransaction { + reserve_pub, + amount, + .. + } if reserve_pub == &Base32::from(reserve_pub_key) && amount == &taler_test_amount + ) + }) + .is_some()); + + println!("Get back some money"); + let wtid = rand_slice(); + ureq::post(&format!("{}/transfer", base_url)) + .send_json(TransferRequest { + request_uid: Base32::from(rand_slice()), + amount: taler_test_amount.clone(), + exchange_base_url: state.config.base_url.clone(), + wtid: Base32::from(wtid), + credit_account: btc_payto_url(&client_addr), + }) + .unwrap(); + + wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); + + println!("Check balances"); + let last_client_balance = client_rpc.get_balance().unwrap(); + assert_eq!(new_client_balance + test_amount, last_client_balance); + + println!("Check history"); + let history: OutgoingHistory = ureq::get(&format!("{}/history/outgoing", base_url)) + .query("delta", "-5") + .call() + .unwrap() + .into_json() + .unwrap(); + assert!(history + .outgoing_transactions + .iter() + .find(|h| { + h.wtid == Base32::from(wtid) + && h.exchange_base_url == state.config.base_url + && h.amount == taler_test_amount + }) + .is_some()); +} diff --git a/instrumentation/src/main.rs b/instrumentation/src/main.rs @@ -14,32 +14,13 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, io::Write, path::PathBuf, time::Duration}; +use std::{fmt::Display, io::Write, path::PathBuf}; -use bitcoin::{Amount, Network, SignedAmount}; -use btc_wire::{ - config::BitcoinConfig, - config_bounce_fee, - metadata::OutMetadata, - rpc::{self, Category, ErrorCode, Rpc}, - rpc_utils::{self, default_data_dir}, - taler_utils::{btc_payto_url, btc_to_taler}, -}; +use btc::btc_test; use clap::StructOpt; -use common::{ - api_common::Base32, - api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, - config::{load_btc_config, Config, GatewayConfig}, - rand_slice, - url::Url, -}; +use common::config::{Config, GatewayConfig}; -pub const CLIENT: &str = "client"; -pub const WIRE: &str = "wire"; - -fn unsigned(amount: SignedAmount) -> Amount { - amount.abs().to_unsigned().unwrap() -} +mod btc; fn print_now(disp: impl Display) { print!("{}", disp); @@ -61,232 +42,7 @@ pub fn main() { let base_url = format!("http://localhost:{}", gateway_conf.port); match gateway_conf.core.currency.as_str() { - "BTC" => { - let config = load_btc_config(args.config.as_deref()); - let data_dir = config - .core - .data_dir - .as_ref() - .cloned() - .unwrap_or_else(default_data_dir); - let btc_config = BitcoinConfig::load(&data_dir).unwrap(); - - if btc_config.network == Network::Bitcoin { - panic!("You should never run this test on a real bitcoin network"); - } - - let bounce_fee = config_bounce_fee(&config); - let min_fund = Amount::from_sat(10_000); - - let mut rpc = Rpc::common(&btc_config).unwrap(); - // Load client - match rpc.load_wallet(CLIENT) { - Ok(_) => {} - Err(rpc::Error::RPC { - code: ErrorCode::RpcWalletNotFound, - .. - }) => { - rpc.create_wallet(CLIENT, "").unwrap(); - } - Err(rpc::Error::RPC { - code: ErrorCode::RpcWalletError | ErrorCode::RpcWalletAlreadyLoaded, - .. - }) => {} - Err(e) => panic!("{}", e), - }; - let mut client_rpc = Rpc::wallet(&btc_config, CLIENT).unwrap(); - let client_addr = client_rpc.gen_addr().unwrap(); - if client_rpc.get_balance().unwrap() < min_fund { - println!("Client need a minimum of {} BTC to run this test, send coins to this address: {}", min_fund.as_btc(), client_addr); - print_now("Waiting for fund:"); - while client_rpc.get_balance().unwrap() < min_fund { - client_rpc.wait_for_new_block().unwrap(); - print_now("."); - } - } - let mut since = client_rpc.list_since_block(None, 1).unwrap().lastblock; - // Load wire - let mut wire_rpc = Rpc::wallet(&btc_config, WIRE).unwrap(); - let wire_addr = wire_rpc.gen_addr().unwrap(); - - print_now("Wait for pending transactions mining:"); - 'l: loop { - let sync = client_rpc.list_since_block(Some(&since), 1).unwrap(); - let sync2 = wire_rpc.list_since_block(Some(&since), 1).unwrap(); - since = sync.lastblock; - for tx in sync - .transactions - .into_iter() - .chain(sync2.transactions.into_iter()) - { - if tx.confirmations == 0 { - rpc.wait_for_new_block().unwrap(); - print_now("."); - continue 'l; - } - } - break; - } - println!(""); - - // Load balances - let client_balance = client_rpc.get_balance().unwrap(); - let wire_balance = wire_rpc.get_balance().unwrap(); - // Test amount - let test_amount = Amount::from_sat(2000); - let min_send_amount = rpc_utils::segwit_min_amount(); // To small to send back - let min_bounce_amount = rpc_utils::segwit_min_amount() + Amount::from_sat(999); // To small after bounce fee - let taler_test_amount = btc_to_taler(&test_amount.to_signed().unwrap()); - - println!("Send transaction"); - let reserve_pub_key = rand_slice(); - let deposit_id = client_rpc - .send_segwit_key(&wire_addr, &test_amount, &reserve_pub_key) - .unwrap(); - let bounce_min_id = client_rpc - .send(&wire_addr, &min_bounce_amount, None, false) - .unwrap(); - let send_min_id = client_rpc - .send(&wire_addr, &min_send_amount, None, false) - .unwrap(); - let bounce_id = client_rpc - .send(&wire_addr, &test_amount, None, false) - .unwrap(); - let client_sent_amount_cost = test_amount - + min_send_amount * 2 - + min_bounce_amount - + min_send_amount - + test_amount; - let client_sent_fees_cost: Amount = [deposit_id, bounce_min_id, send_min_id, bounce_id] - .into_iter() - .map(|id| unsigned(client_rpc.get_tx(&id).unwrap().fee.unwrap())) - .reduce(|acc, i| acc + i) - .unwrap(); - let new_balance = client_rpc.get_balance().unwrap(); - assert_eq!( - client_balance - client_sent_amount_cost - client_sent_fees_cost, - new_balance - ); - - print_now("Wait for client mining and bounce:"); - let mut pending = true; - let mut bounced_id = None; - while pending || bounced_id.is_none() { - pending = false; - std::thread::sleep(Duration::from_secs(1)); - rpc.wait_for_new_block().unwrap(); - print_now("."); - let sync = client_rpc.list_since_block(Some(&since), 1).unwrap(); - for tx in sync.transactions { - if tx.confirmations == 0 { - pending = true; - } else if tx.category == Category::Receive { - let (_, metadata) = client_rpc.get_tx_op_return(&tx.txid).unwrap(); - let metadata = OutMetadata::decode(&metadata).unwrap(); - match metadata { - OutMetadata::Withdraw { .. } => {} - OutMetadata::Bounce { bounced } => { - if bounced == bounce_id { - bounced_id.replace(tx.txid); - } else if bounced == send_min_id { - panic!("Bounced send min"); - } else if bounced == bounce_min_id { - panic!("Bounced bounce min"); - } - } - } - } - } - since = sync.lastblock; - } - println!(""); - - println!("Check balance"); - let new_client_balance = client_rpc.get_balance().unwrap(); - let new_wire_balance = wire_rpc.get_balance().unwrap(); - let bounced = client_rpc.get_tx(&bounced_id.unwrap()).unwrap(); - assert_eq!( - client_balance - client_sent_amount_cost - client_sent_fees_cost - + unsigned(bounced.amount), - new_client_balance - ); - assert_eq!( - wire_balance + test_amount + min_bounce_amount + min_send_amount + bounce_fee, - new_wire_balance - ); - - println!("Check history"); - let history: IncomingHistory = ureq::get(&format!("{}/history/incoming", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history - .incoming_transactions - .iter() - .find(|h| { - matches!( - h, - IncomingBankTransaction::IncomingReserveTransaction { - reserve_pub, - amount, - .. - } if reserve_pub == &Base32::from(reserve_pub_key) && amount == &taler_test_amount - ) - }) - .is_some()); - - println!("Get back some money"); - let url = Url::parse("ftp://example.com").unwrap(); - let wtid = rand_slice(); - ureq::post(&format!("{}/transfer", base_url)) - .send_json(TransferRequest { - request_uid: Base32::from(rand_slice()), - amount: taler_test_amount.clone(), - exchange_base_url: url.clone(), - wtid: Base32::from(wtid), - credit_account: btc_payto_url(&client_addr), - }) - .unwrap(); - - print_now("Wait for wire mining:"); - std::thread::sleep(Duration::from_secs(1)); - 'l1: loop { - let sync = wire_rpc.list_since_block(Some(&since), 1).unwrap(); - since = sync.lastblock; - for tx in sync.transactions { - if tx.confirmations == 0 { - rpc.wait_for_new_block().unwrap(); - print_now("."); - continue 'l1; - } - } - break; - } - println!(""); - - println!("Check balances"); - let last_client_balance = client_rpc.get_balance().unwrap(); - assert_eq!(new_client_balance + test_amount, last_client_balance); - - println!("Check history"); - let history: OutgoingHistory = ureq::get(&format!("{}/history/outgoing", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history - .outgoing_transactions - .iter() - .find(|h| { - h.wtid == Base32::from(wtid) - && h.exchange_base_url == url - && h.amount == taler_test_amount - }) - .is_some()); - } + "BTC" => btc_test(args.config.as_deref(), &base_url), _ => unimplemented!(), } }