depolymerization

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

commit a29a24356438bdbb3d2e342c066d47e66eeb2c81
parent e32c20e7c0d488116a6f020e4b730b0495d3ab9f
Author: Antoine A <>
Date:   Wed, 20 Jul 2022 19:34:59 +0200

Rewriting tests in rust

more robust
less reliance on waiting times
support of variable transaction fees
better platform support

Diffstat:
MCargo.lock | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbtc-wire/benches/metadata.rs | 78+++++++++++++++++++++++++++++++++++++++---------------------------------------
Dbtc-wire/src/bin/btc-wire-utils.rs | 134-------------------------------------------------------------------------------
Mbtc-wire/src/bin/segwit-demo.rs | 10++++++----
Mbtc-wire/src/loops.rs | 3+--
Mbtc-wire/src/loops/analysis.rs | 6++----
Mbtc-wire/src/loops/watcher.rs | 2+-
Mbtc-wire/src/loops/worker.rs | 4++--
Mbtc-wire/src/rpc.rs | 31++++++++++++++++++++++---------
Mcommon/src/sql.rs | 8+++++---
Mcommon/src/status.rs | 2+-
Deth-wire/src/bin/eth-wire-utils.rs | 234-------------------------------------------------------------------------------
Meth-wire/src/loops/analysis.rs | 2+-
Meth-wire/src/loops/worker.rs | 12++++++++++--
Meth-wire/src/rpc.rs | 9+++++++--
Minstrumentation/Cargo.toml | 11+++++++++++
Ainstrumentation/conf/bitcoin.conf | 10++++++++++
Ainstrumentation/conf/bitcoin2.conf | 10++++++++++
Rtest/conf/bitcoin_auth0.conf -> instrumentation/conf/bitcoin_auth0.conf | 0
Ainstrumentation/conf/bitcoin_auth1.conf | 13+++++++++++++
Rtest/conf/bitcoin_auth2.conf -> instrumentation/conf/bitcoin_auth2.conf | 0
Rtest/conf/bitcoin_auth3.conf -> instrumentation/conf/bitcoin_auth3.conf | 0
Rtest/conf/bitcoin_auth4.conf -> instrumentation/conf/bitcoin_auth4.conf | 0
Rtest/conf/bitcoin_auth5.conf -> instrumentation/conf/bitcoin_auth5.conf | 0
Rtest/conf/taler_btc.conf -> instrumentation/conf/taler_btc.conf | 0
Rtest/conf/taler_btc_auth.conf -> instrumentation/conf/taler_btc_auth.conf | 0
Rtest/conf/taler_btc_bump.conf -> instrumentation/conf/taler_btc_bump.conf | 0
Ainstrumentation/conf/taler_btc_lifetime.conf | 15+++++++++++++++
Rtest/conf/taler_eth.conf -> instrumentation/conf/taler_eth.conf | 0
Rtest/conf/taler_eth_bump.conf -> instrumentation/conf/taler_eth_bump.conf | 0
Rtest/conf/taler_eth_lifetime.conf -> instrumentation/conf/taler_eth_lifetime.conf | 0
Minstrumentation/src/btc.rs | 961++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Minstrumentation/src/eth.rs | 990+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Ainstrumentation/src/gateway.rs | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minstrumentation/src/main.rs | 119+++++++++++++++++++++++++++++++++----------------------------------------------
Ainstrumentation/src/utils.rs | 444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmakefile | 35-----------------------------------
Mscript/prepare.sh | 2+-
Dtest/btc/analysis.sh | 64----------------------------------------------------------------
Dtest/btc/bumpfee.sh | 113-------------------------------------------------------------------------------
Dtest/btc/config.sh | 28----------------------------
Dtest/btc/conflict.sh | 111-------------------------------------------------------------------------------
Dtest/btc/hell.sh | 113-------------------------------------------------------------------------------
Dtest/btc/lifetime.sh | 51---------------------------------------------------
Dtest/btc/maxfee.sh | 65-----------------------------------------------------------------
Dtest/btc/reconnect.sh | 87-------------------------------------------------------------------------------
Dtest/btc/reorg.sh | 115-------------------------------------------------------------------------------
Dtest/btc/stress.sh | 124-------------------------------------------------------------------------------
Dtest/btc/wire.sh | 71-----------------------------------------------------------------------
Dtest/common.sh | 405-------------------------------------------------------------------------------
Dtest/conf/bitcoin.conf | 9---------
Dtest/conf/bitcoin2.conf | 8--------
Dtest/conf/bitcoin_auth1.conf | 13-------------
Dtest/conf/taler_btc_lifetime.conf | 15---------------
Dtest/eth/analysis.sh | 64----------------------------------------------------------------
Dtest/eth/bumpfee.sh | 111-------------------------------------------------------------------------------
Dtest/eth/hell.sh | 108-------------------------------------------------------------------------------
Dtest/eth/lifetime.sh | 49-------------------------------------------------
Dtest/eth/maxfee.sh | 66------------------------------------------------------------------
Dtest/eth/reconnect.sh | 91-------------------------------------------------------------------------------
Dtest/eth/reorg.sh | 107-------------------------------------------------------------------------------
Dtest/eth/stress.sh | 110-------------------------------------------------------------------------------
Dtest/eth/test.sh | 17-----------------
Dtest/eth/wire.sh | 63---------------------------------------------------------------
Dtest/gateway/api.sh | 133-------------------------------------------------------------------------------
Dtest/gateway/auth.sh | 31-------------------------------
Mwire-gateway/src/error.rs | 256++++++++++++++++++++++++++++++++++++++++----------------------------------------
67 files changed, 3055 insertions(+), 3040 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,15 @@ version = 3 [[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -57,6 +66,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.5.1", + "object", + "rustc-demangle", +] + +[[package]] name = "base32" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -249,6 +273,17 @@ dependencies = [ ] [[package]] +name = "color-backtrace" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c04463c99389fff045d2b90ce84f5131332712c7ffbede020f5e9ad1ed685" +dependencies = [ + "atty", + "backtrace", + "termcolor", +] + +[[package]] name = "common" version = "0.1.0" dependencies = [ @@ -725,6 +760,12 @@ dependencies = [ ] [[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -904,10 +945,15 @@ dependencies = [ "btc-wire", "clap 3.1.6", "clap_mangen", + "color-backtrace", "common", "eth-wire", "ethereum-types", + "fastrand", "hex", + "libdeflater", + "owo-colors", + "tempfile", "ureq", ] @@ -954,6 +1000,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] +name = "libdeflate-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43afa5b192ff058426ba20a4f35c290ef402478d6045ac934ac15aa947a3898d" +dependencies = [ + "cc", +] + +[[package]] +name = "libdeflater" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e656b7960ec49e864badc7ad1b810427a7ac8b78511a699ce5cdc3ead0b32e5b" +dependencies = [ + "libdeflate-sys", +] + +[[package]] name = "listenfd" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1102,6 +1166,15 @@ dependencies = [ ] [[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] name = "once_cell" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1133,6 +1206,12 @@ dependencies = [ ] [[package]] +name = "owo-colors" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" + +[[package]] name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1453,6 +1532,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1484,6 +1572,12 @@ dependencies = [ ] [[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] name = "rustc-hex" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1744,6 +1838,20 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/btc-wire/benches/metadata.rs b/btc-wire/benches/metadata.rs @@ -1,39 +1,39 @@ -/* - 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 btc_wire::segwit::{decode_segwit_msg, encode_segwit_key, rand_addresses}; -use criterion::{criterion_group, criterion_main, Criterion}; -use common::rand_slice; - -fn criterion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("SegWit addresses"); - group.bench_function("encode", |b| { - b.iter_batched( - || rand_slice(), - |key| encode_segwit_key("bench", &key), - criterion::BatchSize::SmallInput, - ); - }); - group.bench_function("decode", |b| { - b.iter_batched( - || rand_addresses("bench", &rand_slice()), - |addrs| decode_segwit_msg(&addrs), - criterion::BatchSize::SmallInput, - ); - }); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); +/* + 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 btc_wire::segwit::{decode_segwit_msg, encode_segwit_key, rand_addresses}; +use common::rand_slice; +use criterion::{criterion_group, criterion_main, Criterion}; + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("SegWit addresses"); + group.bench_function("encode", |b| { + b.iter_batched( + || rand_slice(), + |key| encode_segwit_key("bench", &key), + criterion::BatchSize::SmallInput, + ); + }); + group.bench_function("decode", |b| { + b.iter_batched( + || rand_addresses("bench", &rand_slice()), + |addrs| decode_segwit_msg(&addrs), + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/btc-wire/src/bin/btc-wire-utils.rs b/btc-wire/src/bin/btc-wire-utils.rs @@ -1,134 +0,0 @@ -/* - 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::PathBuf; - -use bitcoin::{Address, Amount, BlockHash, Network}; -use btc_wire::{ - btc_config::BitcoinConfig, - load_taler_config, - rpc::{Category, Rpc}, -}; -use clap::StructOpt; -use common::{postgres::NoTls, rand_slice}; - -/// btc-wire test utils -#[derive(clap::Parser, Debug)] -#[clap(name = "btc-wire-utils")] -struct Args { - /// Override default configuration file path - #[clap(global = true, short, long)] - config: Option<PathBuf>, - /// Override default data directory path - #[clap(global = true, short, long)] - datadir: Option<PathBuf>, - #[clap(subcommand)] - cmd: Cmd, -} - -#[derive(clap::Subcommand, Debug)] -enum Cmd { - /// Perform wire credit transactions - Transfer { - #[clap(short, long, default_value_t = String::from("client"))] - /// sender wallet - from: String, - #[clap(short, long, default_value_t = String::from("wire"))] - /// receiver wallet - to: String, - /// amount to send in btc - amount: f64, - }, - /// Wait or mine the next block - Nblock { - #[clap(default_value_t = String::from("wire"))] - /// receiver wallet - to: String, - }, - /// Abandon all unconfirmed transaction - Abandon { - #[clap(default_value_t = String::from("wire"))] - /// sender wallet - from: String, - }, - /// Clear database - Resetdb, -} - -pub fn auto_wallet(rpc: &mut Rpc, config: &BitcoinConfig, name: &str) -> (Rpc, Address) { - // Auto load - rpc.load_wallet(name).ok(); - let mut wallet = Rpc::wallet(config, name).unwrap(); - let addr = wallet - .gen_addr() - .unwrap_or_else(|_| panic!("Failed to get wallet address {}", name)); - (wallet, addr) -} - -fn main() { - common::log::init(); - let args = Args::parse(); - let (taler_config, path, currency) = load_taler_config(args.config.as_deref()); - let btc_config = BitcoinConfig::load(args.datadir.unwrap_or(path), currency).unwrap(); - let mut rpc = Rpc::common(&btc_config).unwrap(); - - match args.cmd { - Cmd::Transfer { from, to, amount } => { - let (mut client, _) = auto_wallet(&mut rpc, &btc_config, &from); - let (_, to) = auto_wallet(&mut rpc, &btc_config, &to); - let tx = client - .send_segwit_key(&to, &Amount::from_btc(amount).unwrap(), &rand_slice()) - .unwrap(); - println!("{}", tx); - } - Cmd::Nblock { to } => { - match btc_config.network { - Network::Regtest => { - // Manually mine a block - let (_, addr) = auto_wallet(&mut rpc, &btc_config, &to); - rpc.mine(1, &addr).unwrap(); - } - _ => { - // Wait for next network block - rpc.wait_for_new_block().ok(); - } - } - } - Cmd::Abandon { from } => { - let (mut wire, _) = auto_wallet(&mut rpc, &btc_config, &from); - let list = wire.list_since_block(None, 1).unwrap(); - for tx in list.transactions { - if tx.category == Category::Send && tx.confirmations == 0 { - wire.abandon_tx(&tx.txid).unwrap(); - } - } - } - Cmd::Resetdb => { - let hash: BlockHash = rpc.get_genesis().unwrap(); - let mut db = taler_config.db_config().connect(NoTls).unwrap(); - let mut tx = db.transaction().unwrap(); - // Clear transaction tables and reset state - tx.execute("DELETE FROM tx_in", &[]).unwrap(); - tx.execute("DELETE FROM tx_out", &[]).unwrap(); - tx.execute("DELETE FROM bounce", &[]).unwrap(); - tx.execute( - "UPDATE state SET value=$1 WHERE name='last_hash'", - &[&hash.as_ref()], - ) - .unwrap(); - tx.commit().unwrap(); - } - } -} diff --git a/btc-wire/src/bin/segwit-demo.rs b/btc-wire/src/bin/segwit-demo.rs @@ -1,8 +1,8 @@ use std::str::FromStr; use bitcoin::{Address, Amount, Network}; +use btc_wire::segwit::decode_segwit_msg; use btc_wire::{rpc_utils, segwit::encode_segwit_addr}; -use btc_wire::{segwit::decode_segwit_msg}; use common::{ base32::{self, Alphabet}, rand_slice, @@ -81,12 +81,14 @@ pub fn main() { "Make sure the amount show 0.10000588 BTC, else you have to change the base unit to BTC" ); - let key1 = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"; let key2 = "tb1qzxwu2p7urkqx0gq2ltfazf9w2jdu48ya8qwlm0"; let key3 = "tb1qzxwu2pef8a224xagwq8hej8akuvd63yluu3wrh"; let addresses = vec![key1, key2, key3]; let dec = decode_segwit_msg(&addresses); - - println!("Decode reserve public key: 0x{}", hex::encode(&dec.unwrap()[..])); + + println!( + "Decode reserve public key: 0x{}", + hex::encode(&dec.unwrap()[..]) + ); } diff --git a/btc-wire/src/loops.rs b/btc-wire/src/loops.rs @@ -35,4 +35,4 @@ pub enum LoopError { Injected(#[from] Injected), } -pub type LoopResult<T> = Result<T, LoopError>; -\ No newline at end of file +pub type LoopResult<T> = Result<T, LoopError>; diff --git a/btc-wire/src/loops/analysis.rs b/btc-wire/src/loops/analysis.rs @@ -22,8 +22,8 @@ use common::{ reconnect::AutoReconnectDb, }; -use crate::WireState; use super::LoopResult; +use crate::WireState; /// Analyse blockchain behavior and adapt confirmations in real time pub fn analysis(mut rpc: AutoRpcCommon, mut db: AutoReconnectDb, state: &WireState) { @@ -40,9 +40,7 @@ pub fn analysis(mut rpc: AutoRpcCommon, mut db: AutoReconnectDb, state: &WireSta let fork = rpc .get_chain_tips()? .into_iter() - .filter_map(|t| { - (t.status == ChainTipsStatus::ValidFork).then(|| t.length) - }) + .filter_map(|t| (t.status == ChainTipsStatus::ValidFork).then(|| t.length)) .max() .unwrap_or(0) as u32; // The first time we see a fork that big diff --git a/btc-wire/src/loops/watcher.rs b/btc-wire/src/loops/watcher.rs @@ -13,8 +13,8 @@ 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 common::{log::log::error, reconnect::AutoReconnectDb}; use btc_wire::rpc::AutoRpcCommon; +use common::{log::log::error, reconnect::AutoReconnectDb}; use std::time::Duration; use super::LoopResult; diff --git a/btc-wire/src/loops/worker.rs b/btc-wire/src/loops/worker.rs @@ -227,8 +227,8 @@ fn sync_chain( // Move last_hash forward db.execute( - "UPDATE state SET value=$1 WHERE name='last_hash'", - &[&lastblock.as_ref()], + "UPDATE state SET value=$1 WHERE name='last_hash' AND value=$2", + &[&lastblock.as_ref(), &last_hash.as_inner().as_slice()], )?; Ok(Some(stuck)) diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs @@ -47,7 +47,7 @@ pub fn auto_rpc_wallet(config: BitcoinConfig, wallet: &'static str) -> AutoRpcWa let mut rpc = Rpc::wallet(config, wallet) .map_err(|err| error!("connect RPC: {}", err)) .ok()?; - rpc.load_wallet(wallet).ok(); + rpc.load_wallet(wallet).ok(); rpc.unlock_wallet(&password()) .map_err(|err| error!("connect RPC: {}", err)) .ok()?; @@ -114,6 +114,13 @@ pub type Result<T> = std::result::Result<T, Error>; const EMPTY: [(); 0] = []; +fn expect_null(result: Result<()>) -> Result<()> { + match result { + Err(Error::Null) => Ok(()), + i => i, + } +} + /// Bitcoin RPC connection pub struct Rpc { last_call: Instant, @@ -241,10 +248,7 @@ impl Rpc { /// Unlock loaded wallet pub fn unlock_wallet(&mut self, passwd: &str) -> Result<()> { // TODO Capped at 3yrs, is it enough ? - match self.call("walletpassphrase", &(passwd, 100000000)) { - Err(Error::Null) => Ok(()), - i => i, - } + expect_null(self.call("walletpassphrase", &(passwd, 100000000))) } /* ----- Wallet utils ----- */ @@ -367,10 +371,7 @@ impl Rpc { /// Abandon a pending transaction pub fn abandon_tx(&mut self, id: &Txid) -> Result<()> { - match self.call("abandontransaction", &[&id]) { - Err(Error::Null) => Ok(()), - i => i, - } + expect_null(self.call("abandontransaction", &[&id])) } /* ----- Watcher ----- */ @@ -388,6 +389,18 @@ impl Rpc { ) -> Result<ListSinceBlock> { self.call("listsinceblock", &(hash, confirmation.max(1), (), true)) } + + /* ----- Cluster ----- */ + + /// Try a connection to a node once + pub fn add_node(&mut self, addr: &str) -> Result<()> { + expect_null(self.call("addnode", &(addr, "onetry"))) + } + + /// Immediately disconnects from the specified peer node. + pub fn disconnect_node(&mut self, addr: &str) -> Result<()> { + expect_null(self.call("disconnectnode", &(addr, ()))) + } } #[derive(Debug, serde::Deserialize)] diff --git a/common/src/sql.rs b/common/src/sql.rs @@ -19,7 +19,10 @@ use std::str::FromStr; use postgres::Row; use url::Url; -use crate::{api_common::{Amount, SafeU64}, log::OrFail}; +use crate::{ + api_common::{Amount, SafeU64}, + log::OrFail, +}; /// URL from sql pub fn sql_url(row: &Row, idx: usize) -> Url { @@ -30,8 +33,7 @@ pub fn sql_url(row: &Row, idx: usize) -> Url { /// Ethereum amount from sql pub fn sql_amount(row: &Row, idx: usize) -> Amount { let str: &str = row.get(idx); - Amount::from_str(str) - .or_fail(|_| format!("Database invariant: expected an amount got {}", str)) + Amount::from_str(str).or_fail(|_| format!("Database invariant: expected an amount got {}", str)) } /// Byte array from sql diff --git a/common/src/status.rs b/common/src/status.rs @@ -16,7 +16,7 @@ //! Transactions status in database /// Debit transaction status -/// +/// /// -> Requested API request /// Requested -> Sent Announced to the bitcoin network /// Sent -> Requested Conflicting transaction (reorg) diff --git a/eth-wire/src/bin/eth-wire-utils.rs b/eth-wire/src/bin/eth-wire-utils.rs @@ -1,234 +0,0 @@ -/* - 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::PathBuf, - process::exit, - str::FromStr, - time::{Duration, Instant}, -}; - -use clap::StructOpt; -use common::{api_common::Amount, log::init, password, postgres::NoTls, rand_slice}; -use eth_wire::{ - load_taler_config, - rpc::{hex::Hex, Rpc, RpcClient, TransactionRequest}, - taler_util::{taler_to_eth, TRUNC}, - RpcExtended, SyncState, -}; -use ethereum_types::{H160, U256}; - -#[derive(clap::Parser, Debug)] -#[clap(name = "eth-wire-utils")] -/// eth-wire test utils -struct Args { - /// Override default configuration file path - #[clap(global = true, short, long)] - config: Option<PathBuf>, - /// Override default data directory path - #[clap(global = true, short, long)] - datadir: Option<PathBuf>, - #[clap(subcommand)] - cmd: Cmd, -} - -#[derive(clap::Parser, Debug)] -struct TransactionCmd { - /// sender wallet - from: String, - /// receiver wallet - to: String, - /// sender wallet - fmt: String, - /// amounts to send in eth - amounts: Vec<u32>, -} - -#[derive(clap::Subcommand, Debug)] -enum Cmd { - /// Send common ethereum transactions - Send(TransactionCmd), - /// Send taler credit transactions - Credit(TransactionCmd), - /// Mine pending transactions and more blocks - Mine { - /// receiver wallet - to: String, - #[clap(default_value_t = 0)] - /// amount to mine in eth - amount: u64, - }, - /// Clear database - Resetdb, - /// Get eth balance - Balance { - /// account address - addr: String, - }, - // Check client and wire balance - CheckBalance { - client_addr: String, - client: u64, - wire_addr: String, - wire: u64, - }, - /// Abandon all unconfirmed transaction - Abandon { - /// sender address - from: String, - }, - Export { - path: String, - }, -} - -fn main() { - init(); - let args: Args = Args::parse(); - let (taler_config, ipc_path, currency) = load_taler_config(args.config.as_deref()); - - let ipc_path = args.datadir.unwrap_or(ipc_path); - let mut rpc = Rpc::new(ipc_path).unwrap(); - let passwd = password(); - match args.cmd { - Cmd::Credit(TransactionCmd { - from, - to, - fmt, - amounts, - }) => { - let from = H160::from_str(&from).unwrap(); - let to = H160::from_str(&to).unwrap(); - rpc.unlock_account(&from, &passwd).ok(); - for amount in amounts { - let amount = - Amount::from_str(&format!("{}:{}{}", currency.to_str(), fmt, amount)).unwrap(); - let value = taler_to_eth(&amount, currency).unwrap(); - rpc.credit(from, to, value, rand_slice()).unwrap(); - } - } - Cmd::Send(TransactionCmd { - from, - to, - fmt, - amounts, - }) => { - let from = H160::from_str(&from).unwrap(); - let to = H160::from_str(&to).unwrap(); - rpc.unlock_account(&from, &passwd).ok(); - for amount in amounts { - let amount = - Amount::from_str(&format!("{}:{}{}", currency.to_str(), fmt, amount)).unwrap(); - let value = taler_to_eth(&amount, currency).unwrap(); - rpc.send_transaction(&TransactionRequest { - from, - to, - value, - nonce: None, - gas_price: None, - data: Hex(vec![]), - }) - .unwrap(); - } - } - Cmd::Mine { to, mut amount } => { - let to = H160::from_str(&to).unwrap(); - rpc.unlock_account(&to, &passwd).ok(); - let mut rpc = rpc.subscribe_new_head().unwrap(); - - rpc.miner_start().unwrap(); - while !rpc.pending_transactions().unwrap().is_empty() { - rpc.next().unwrap(); - amount = amount.saturating_sub(1); - } - for _ in 0..amount { - rpc.next().unwrap(); - } - rpc.miner_stop().unwrap(); - } - Cmd::Resetdb => { - let block = rpc.earliest_block().unwrap(); - let mut db = taler_config.db_config().connect(NoTls).unwrap(); - let mut tx = db.transaction().unwrap(); - // Clear transaction tables and reset state - tx.execute("DELETE FROM tx_in", &[]).unwrap(); - tx.execute("DELETE FROM tx_out", &[]).unwrap(); - tx.execute("DELETE FROM bounce", &[]).unwrap(); - tx.execute( - "UPDATE state SET value=$1 WHERE name='sync'", - &[&SyncState { - tip_hash: block.hash.unwrap(), - tip_height: block.number.unwrap(), - conf_height: block.number.unwrap(), - } - .to_bytes() - .as_ref()], - ) - .unwrap(); - tx.commit().unwrap(); - } - Cmd::Balance { addr } => { - let addr = H160::from_str(&addr).unwrap(); - let balance = rpc.get_balance(&addr).unwrap(); - println!("{}", (balance / TRUNC).as_u64()); - } - Cmd::CheckBalance { - client_addr, - client, - wire_addr, - wire, - } => { - let start = Instant::now(); - let client_addr = H160::from_str(&client_addr).unwrap(); - let wire_addr = H160::from_str(&wire_addr).unwrap(); - loop { - let client_balance = (rpc.get_balance(&client_addr).unwrap() / TRUNC).as_u64(); - let wire_balance = (rpc.get_balance(&wire_addr).unwrap() / TRUNC).as_u64(); - if client_balance == client && wire_balance == wire { - break; - } else if start.elapsed() > Duration::from_secs(60) { - println!( - "Expected {} {} got {} {}", - client, wire, client_balance, wire_balance - ); - exit(1); - } else { - std::thread::sleep(Duration::from_secs(5)) - } - } - } - Cmd::Abandon { from } => { - let from = H160::from_str(&from).unwrap(); - rpc.unlock_account(&from, &passwd).ok(); - let pending = rpc.pending_transactions().unwrap(); - for tx in pending.into_iter().filter(|t| t.from == Some(from)) { - // Replace transaction value with 0 - rpc.send_transaction(&TransactionRequest { - from, - to: tx.to.unwrap(), - value: U256::zero(), - gas_price: Some(U256::from(1u8)), // Bigger gas price to replace fee - data: Hex(vec![]), - nonce: Some(tx.nonce), - }) - .unwrap(); - } - } - Cmd::Export { path } => { - std::fs::remove_file(&path).ok(); - assert!(rpc.export_chain(&path).unwrap()) - } - } -} diff --git a/eth-wire/src/loops/analysis.rs b/eth-wire/src/loops/analysis.rs @@ -21,7 +21,7 @@ use common::{ postgres::fallible_iterator::FallibleIterator, reconnect::AutoReconnectDb, }; -use eth_wire::rpc::{ Rpc, AutoRpcCommon, RpcClient}; +use eth_wire::rpc::{AutoRpcCommon, Rpc, RpcClient}; use ethereum_types::{H256, U64}; use crate::WireState; diff --git a/eth-wire/src/loops/worker.rs b/eth-wire/src/loops/worker.rs @@ -382,7 +382,11 @@ fn sync_chain_outgoing(tx: &SyncTransaction, db: &mut Client, state: &WireState) ], )?; if nb_row > 0 { - warn!("|| (recovered) {} in {}", &bounced, hex::encode(tx.hash)); + warn!( + "|| (recovered) {} in {}", + hex::encode(bounced), + hex::encode(tx.hash) + ); } } BounceStatus::Ignored => error!( @@ -399,7 +403,11 @@ fn sync_chain_outgoing(tx: &SyncTransaction, db: &mut Client, state: &WireState) &[&bounced.as_ref(), &tx.hash.as_ref(), &(BounceStatus::Sent as i16)], )?; if nb > 0 { - warn!("|| (onchain) {} in {}", &bounced, hex::encode(tx.hash)); + warn!( + "|| (onchain) {} in {}", + hex::encode(bounced), + hex::encode(tx.hash) + ); } } } diff --git a/eth-wire/src/rpc.rs b/eth-wire/src/rpc.rs @@ -372,11 +372,16 @@ pub trait RpcClient { self.call("eth_getBlockByNumber", &("earliest", &true)) } - /// Get account balance - fn get_balance(&mut self, addr: &Address) -> Result<U256> { + /// Get latest account balance + fn get_balance_latest(&mut self, addr: &Address) -> Result<U256> { self.call("eth_getBalance", &(addr, "latest")) } + /// Get pending account balance + fn get_balance_pending(&mut self, addr: &Address) -> Result<U256> { + self.call("eth_getBalance", &(addr, "pending")) + } + /// Get node info fn node_info(&mut self) -> Result<NodeInfo> { self.call("admin_nodeInfo", &EMPTY) diff --git a/instrumentation/Cargo.toml b/instrumentation/Cargo.toml @@ -18,6 +18,17 @@ ethereum-types = { version = "0.13.1", default-features = false } hex = "0.4.3" # Wire Gateway ureq = { version = "2.4.0", features = ["json"] } +# In memory deflate library +libdeflater = "0.10.0" +# Generate temporary files +tempfile = "3.3.0" +# RNG +fastrand = "1.7.0" +# terminal color +owo-colors = "3.4.0" +# Better backtrace +color-backtrace = "0.5.1" + [build-dependencies] clap_mangen = "0.1" diff --git a/instrumentation/conf/bitcoin.conf b/instrumentation/conf/bitcoin.conf @@ -0,0 +1,9 @@ +regtest=1 +txindex=1 +maxtxfee=0.01 +fallbackfee=0.00000001 +rpcservertimeout=10 + +[regtest] +port=8345 +rpcport=18345 +\ No newline at end of file diff --git a/instrumentation/conf/bitcoin2.conf b/instrumentation/conf/bitcoin2.conf @@ -0,0 +1,9 @@ +regtest=1 +txindex=1 +maxtxfee=0.01 +fallbackfee=0.00000001 +rpcservertimeout=0 + +[regtest] +port=8346 +rpcport=18346 +\ No newline at end of file diff --git a/test/conf/bitcoin_auth0.conf b/instrumentation/conf/bitcoin_auth0.conf diff --git a/instrumentation/conf/bitcoin_auth1.conf b/instrumentation/conf/bitcoin_auth1.conf @@ -0,0 +1,12 @@ +regtest=1 +txindex=1 +maxtxfee=0.1 +fallbackfee=0.00000001 +rpcuser=bob +rpcpassword=password + +[regtest] +port=8346 +rpcport=18346 +rpcuser=alice +rpcpassword=password TODO +\ No newline at end of file diff --git a/test/conf/bitcoin_auth2.conf b/instrumentation/conf/bitcoin_auth2.conf diff --git a/test/conf/bitcoin_auth3.conf b/instrumentation/conf/bitcoin_auth3.conf diff --git a/test/conf/bitcoin_auth4.conf b/instrumentation/conf/bitcoin_auth4.conf diff --git a/test/conf/bitcoin_auth5.conf b/instrumentation/conf/bitcoin_auth5.conf diff --git a/test/conf/taler_btc.conf b/instrumentation/conf/taler_btc.conf diff --git a/test/conf/taler_btc_auth.conf b/instrumentation/conf/taler_btc_auth.conf diff --git a/test/conf/taler_btc_bump.conf b/instrumentation/conf/taler_btc_bump.conf diff --git a/instrumentation/conf/taler_btc_lifetime.conf b/instrumentation/conf/taler_btc_lifetime.conf @@ -0,0 +1,14 @@ +[taler] +CURRENCY = DEVBTC + +[exchange] +BASE_URL = http://test.com + +[depolymerizer-bitcoin] +DB_URL = postgres://localhost:5454/postgres?user=postgres&password=password +PORT = 8060 +PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj +CONFIRMATION = 3 +HTTP_LIFETIME = 10 +WIRE_LIFETIME = 110 +AUTH_METHOD = none +\ No newline at end of file diff --git a/test/conf/taler_eth.conf b/instrumentation/conf/taler_eth.conf diff --git a/test/conf/taler_eth_bump.conf b/instrumentation/conf/taler_eth_bump.conf diff --git a/test/conf/taler_eth_lifetime.conf b/instrumentation/conf/taler_eth_lifetime.conf diff --git a/instrumentation/src/btc.rs b/instrumentation/src/btc.rs @@ -14,18 +14,29 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{path::Path, time::Duration}; +use std::{ + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, + sync::atomic::Ordering, + thread::sleep, + time::Duration, +}; -use bitcoin::{Amount, BlockHash, Network, SignedAmount, Txid, hashes::Hash}; +use bitcoin::{hashes::Hash, Address, Amount, BlockHash, Network, SignedAmount, Txid}; use btc_wire::{ + btc_config::BitcoinConfig, rpc::{self, Category, ErrorCode, Rpc, Transaction}, - rpc_utils, + rpc_utils::{self, segwit_min_amount}, taler_utils::{btc_payto_url, btc_to_taler}, WireState, }; -use common::{rand_slice, metadata::OutMetadata}; +use common::{currency::CurrencyBtc, metadata::OutMetadata, postgres::NoTls, rand_slice}; -use crate::{check_incoming, check_outgoing, print_now, transfer}; +use crate::utils::{ + check_incoming, check_outgoing, cmd_redirect, cmd_redirect_ok, print_now, retry, retry_opt, + transfer, ChildGuard, CommonCtx, Dirs, +}; pub const CLIENT: &str = "client"; pub const WIRE: &str = "wire"; @@ -54,7 +65,7 @@ fn wait_for_pending(since: &mut BlockHash, client_rpc: &mut Rpc, wire_rpc: &mut } break; } - println!(""); + println!(); } pub fn btc_test(config: Option<&Path>, base_url: &str) { @@ -95,7 +106,7 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { client_rpc.wait_for_new_block().unwrap(); print_now("."); } - println!(""); + println!(); } let mut since = client_rpc.list_since_block(None, 1).unwrap().lastblock; // Load wire @@ -166,7 +177,7 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { rpc.wait_for_new_block().unwrap(); print_now("."); }; - println!(""); + println!(); wait_for_pending(&mut since, &mut client_rpc, &mut wire_rpc); println!("Check balance"); @@ -181,7 +192,11 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { new_wire_balance ); - check_incoming(base_url, &reserve_pub_key, &taler_test_amount); + println!("Check incoming history"); + assert!(check_incoming( + base_url, + &[(reserve_pub_key, taler_test_amount.clone())] + )); println!("Get back some money"); let wtid = rand_slice(); @@ -198,5 +213,931 @@ pub fn btc_test(config: Option<&Path>, base_url: &str) { let last_client_balance = client_rpc.get_balance().unwrap(); assert_eq!(new_client_balance + test_amount, last_client_balance); - check_outgoing(base_url, &wtid, &state.base_url, &taler_test_amount); + println!("Check outgoing history"); + assert!(check_outgoing( + base_url, + &state.base_url, + &[(wtid, taler_test_amount)] + )); +} + +pub struct BtcCtx { + btc_node: ChildGuard, + _btc_node2: ChildGuard, + common_rpc: Rpc, + common_rpc2: Rpc, + wire_rpc: Rpc, + client_rpc: Rpc, + reserve_rpc: Rpc, + wire_addr: Address, + pub client_addr: Address, + reserve_addr: Address, + state: WireState, + conf: u16, + common: CommonCtx, +} + +impl Deref for BtcCtx { + type Target = CommonCtx; + + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for BtcCtx { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +impl BtcCtx { + pub fn setup(config: &str, stressed: bool) -> Self { + Self::_setup(config, stressed) + } + + pub fn config(config: &str) { + std::fs::create_dir_all("log").unwrap(); + for file in std::fs::read_dir("log").unwrap() { + std::fs::write(file.unwrap().path(), "").unwrap(); + } + // Generate temporary dirs + let dirs = Dirs::generate(); + // Bitcoin config + let config = PathBuf::from_str("instrumentation/conf") + .unwrap() + .join(config); + std::fs::copy(config, dirs.wire_dir.join("bitcoin.conf")).unwrap(); + // Load config + let config = + BitcoinConfig::load(dirs.wire_dir.join("bitcoin.conf"), CurrencyBtc::Dev).unwrap(); + // Start bitcoin nodes + let _btc_node = cmd_redirect( + "bitcoind", + &[&format!("-datadir={}", dirs.wire_dir.to_string_lossy())], + "log/node.log", + ); + // Connect + retry(|| { + Rpc::common(&config) + .ok() + .and_then(|mut it| it.get_blockchain_info().ok()) + .is_some() + }) + } + + fn _setup(taler_config: &str, stressed: bool) -> Self { + let dirs = CommonCtx::setup_dirs(taler_config); + // Prepare config + let (btc_config2, state) = { + // Bitcoin config + let config = PathBuf::from_str("instrumentation/conf") + .unwrap() + .join("bitcoin.conf"); + std::fs::copy(config, dirs.wire_dir.join("bitcoin.conf")).unwrap(); + std::fs::copy( + "instrumentation/conf/bitcoin2.conf", + dirs.wire_dir2.join("bitcoin.conf"), + ) + .unwrap(); + // Load config + let state = WireState::load_taler_config(Some(&dirs.conf)); + ( + BitcoinConfig::load(dirs.wire_dir2.join("bitcoin.conf"), state.currency).unwrap(), + state, + ) + }; + // Start bitcoin nodes + let btc_node = cmd_redirect( + "bitcoind", + &[&format!("-datadir={}", dirs.wire_dir.to_string_lossy())], + "log/node.log", + ); + let btc_node2 = cmd_redirect( + "bitcoind", + &[&format!("-datadir={}", dirs.wire_dir2.to_string_lossy())], + "log/node2.log", + ); + + let common = CommonCtx::setup(dirs, "btc-wire", stressed, |dirs| { + // Generate wallet + cmd_redirect_ok( + "btc-wire", + &["-c", dirs.conf.to_str().unwrap(), "initwallet"], + "log/cmd.log", + "wire initwallet", + ); + }); + + // Setup wallets + let mut common_rpc = retry_opt(|| Rpc::common(&state.btc_config).ok()); + common_rpc.add_node("127.0.0.1:8346").unwrap(); + + for name in ["client", "reserve"] { + common_rpc.create_wallet(name, "").unwrap(); + } + let common_rpc2 = retry_opt(|| Rpc::common(&btc_config2).ok()); + + // Generate money + let mut reserve_rpc = Rpc::wallet(&state.btc_config, "reserve").unwrap(); + let mut client_rpc = Rpc::wallet(&state.btc_config, "client").unwrap(); + let mut wire_rpc = Rpc::wallet(&state.btc_config, "wire").unwrap(); + let reserve_addr = reserve_rpc.gen_addr().unwrap(); + let client_addr = client_rpc.gen_addr().unwrap(); + let wire_addr = wire_rpc.gen_addr().unwrap(); + common_rpc.mine(101, &reserve_addr).unwrap(); + reserve_rpc + .send(&client_addr, &(Amount::ONE_BTC * 10), None, false) + .unwrap(); + common_rpc.mine(1, &reserve_addr).unwrap(); + + let conf = state.confirmation.load(Ordering::SeqCst) as u16; + Self { + common, + btc_node, + common_rpc, + wire_rpc, + client_rpc, + reserve_rpc, + wire_addr, + client_addr, + reserve_addr, + state, + conf, + _btc_node2: btc_node2, + common_rpc2, + } + } + + pub fn reset_db(&mut self) { + let hash: BlockHash = self.common_rpc.get_genesis().unwrap(); + let mut db = self.common.taler_conf.db_config().connect(NoTls).unwrap(); + let mut tx = db.transaction().unwrap(); + // Clear transaction tables and reset state + tx.batch_execute("DELETE FROM tx_in;DELETE FROM tx_out;DELETE FROM bounce;") + .unwrap(); + tx.execute( + "UPDATE state SET value=$1 WHERE name='last_hash'", + &[&hash.as_ref()], + ) + .unwrap(); + tx.commit().unwrap(); + } + + pub fn stop_node(&mut self) { + // We need to kill bitcoin gracefully to avoid corruption + #[cfg(unix)] + { + cmd_redirect_ok( + "kill", + &[&self.btc_node.0.id().to_string()], + "/dev/null", + "fill btc node", + ); + self.btc_node.0.wait().unwrap(); + } + } + + pub fn cluster_deco(&mut self) { + self.common_rpc.disconnect_node("127.0.0.1:8346").unwrap(); + } + + pub fn cluster_fork(&mut self, length: u16) { + self.common_rpc2.mine(length, &self.reserve_addr).unwrap(); + self.common_rpc.add_node("127.0.0.1:8346").unwrap(); + } + + pub fn restart_node(&mut self, additional_args: &[&str]) { + self.stop_node(); + self.resume_node(additional_args); + } + + pub fn resume_node(&mut self, additional_args: &[&str]) { + let datadir = format!("-datadir={}", self.common.dirs.wire_dir.to_string_lossy()); + let mut args = vec![datadir.as_str()]; + args.extend_from_slice(additional_args); + self.btc_node = cmd_redirect("bitcoind", &args, "log/node.log"); + self.common_rpc = retry_opt(|| Rpc::common(&self.state.btc_config).ok()); + self.common_rpc.add_node("127.0.0.1:8346").unwrap(); + for name in ["client", "reserve", "wire"] { + self.common_rpc.load_wallet(name).ok(); + } + + self.reserve_rpc = Rpc::wallet(&self.state.btc_config, "reserve").unwrap(); + self.client_rpc = Rpc::wallet(&self.state.btc_config, "client").unwrap(); + self.wire_rpc = Rpc::wallet(&self.state.btc_config, "wire").unwrap(); + } + + /* ----- Transaction ------ */ + + pub fn credit(&mut self, amount: Amount, metadata: [u8; 32]) { + self.client_rpc + .send_segwit_key(&self.wire_addr, &amount, &metadata) + .unwrap(); + } + + pub fn debit(&mut self, amount: Amount, metadata: [u8; 32]) { + transfer( + &self.common.gateway_url, + &metadata, + &self.state.base_url, + btc_payto_url(&self.client_addr), + &btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), + ) + } + + pub fn malformed_credit(&mut self, amount: &Amount) { + self.client_rpc + .send(&self.wire_addr, amount, None, false) + .unwrap(); + } + + pub fn reset_wallet(&mut self) { + let amount = self.wire_balance(); + self.wire_rpc + .send(&self.client_addr, &amount, None, true) + .unwrap(); + self.next_block(); + } + + fn abandon(rpc: &mut Rpc) { + let list = rpc.list_since_block(None, 1).unwrap(); + for tx in list.transactions { + if tx.category == Category::Send && tx.confirmations == 0 { + rpc.abandon_tx(&tx.txid).unwrap(); + } + } + } + + pub fn abandon_wire(&mut self) { + Self::abandon(&mut self.wire_rpc); + } + + pub fn abandon_client(&mut self) { + Self::abandon(&mut self.client_rpc); + } + + /* ----- Mining ----- */ + + fn mine(&mut self, nb: u16) { + self.common_rpc.mine(nb, &self.reserve_addr).unwrap(); + } + + pub fn next_conf(&mut self) { + self.mine(self.conf) + } + + pub fn next_block(&mut self) { + self.mine(1) + } + + /* ----- Balances ----- */ + + pub fn client_balance(&mut self) -> Amount { + self.client_rpc.get_balance().unwrap() + } + + pub fn wire_balance(&mut self) -> Amount { + self.wire_rpc.get_balance().unwrap() + } + + fn expect_balance(&mut self, balance: Amount, mine: bool, lambda: fn(&mut Self) -> Amount) { + retry(|| { + let check = balance == lambda(self); + if !check && mine { + self.next_block(); + } + check + }); + } + + pub fn expect_client_balance(&mut self, balance: Amount, mine: bool) { + self.expect_balance(balance, mine, Self::client_balance) + } + + pub fn expect_wire_balance(&mut self, balance: Amount, mine: bool) { + self.expect_balance(balance, mine, Self::wire_balance) + } + + /* ----- Wire Gateway ----- */ + + pub fn expect_credits(&self, txs: &[([u8; 32], Amount)]) { + let txs: Vec<_> = txs + .iter() + .map(|(metadata, amount)| { + ( + *metadata, + btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), + ) + }) + .collect(); + self.common.expect_credits(&txs) + } + + pub fn expect_debits(&self, txs: &[([u8; 32], Amount)]) { + let txs: Vec<_> = txs + .iter() + .map(|(metadata, amount)| { + ( + *metadata, + btc_to_taler(&amount.to_signed().unwrap(), self.state.currency), + ) + }) + .collect(); + self.common.expect_debits(&self.state.base_url, &txs) + } +} + +pub const TESTS: &[(fn(), &str)] = &[ + (wire, "btc_wire"), + (lifetime, "btc_lifetime"), + (reconnect, "btc_reconnect"), + (stress, "btc_stress"), + (conflict, "btc_conflict"), + (reorg, "btc_reorg"), + (hell, "btc_hell"), + (analysis, "btc_analysis"), + (bumpfee, "btc_bumpfee"), + (maxfee, "btc_maxfee"), + (config, "btc_config"), +]; + +/// Test btc-wire correctly receive and send transactions on the blockchain +fn wire() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + println!("Credit"); + { + // Send transactions + let mut balance = ctx.wire_balance(); + let mut txs = Vec::new(); + for n in 10..100 { + let metadata = rand_slice(); + let amount = Amount::from_sat(n * 1000); + ctx.credit(amount, metadata); + txs.push((metadata, amount)); + balance += amount; + ctx.next_block(); + } + ctx.next_conf(); + ctx.expect_credits(&txs); + ctx.expect_wire_balance(balance, true); + }; + + println!("Debit"); + { + let mut balance = ctx.client_balance(); + let mut txs = Vec::new(); + for n in 10..100 { + let metadata = rand_slice(); + let amount = Amount::from_sat(n * 100); + balance += amount; + ctx.debit(amount, metadata); + txs.push((metadata, amount)); + } + ctx.next_block(); + ctx.expect_debits(&txs); + ctx.expect_client_balance(balance, true); + } + + println!("Bounce"); + { + ctx.reset_wallet(); + // Send bad transactions + let mut balance = ctx.wire_balance(); + for n in 10..40 { + ctx.malformed_credit(&Amount::from_sat(n * 1000)); + balance += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(balance, true); + } +} + +/// Check btc-wire and wire-gateway correctly stop when a lifetime limit is configured +fn lifetime() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc_lifetime.conf", false); + println!("Check lifetime"); + // Start up + retry(|| ctx.wire_running() && ctx.gateway_running()); + // Consume lifetime + for _ in 0..=ctx.taler_conf.wire_lifetime().unwrap() { + ctx.credit(segwit_min_amount(), rand_slice()); + ctx.next_block(); + } + for _ in 0..=ctx.taler_conf.http_lifetime().unwrap() { + ctx.debit(segwit_min_amount(), rand_slice()); + ctx.next_block(); + } + // End down + retry(|| !ctx.wire_running() && !ctx.gateway_running()); +} + +/// Check the capacity of wire-gateway and btc-wire to recover from database and node loss +fn reconnect() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + let mut credits = Vec::new(); + let mut debits = Vec::new(); + + println!("With DB"); + { + let metadata = rand_slice(); + let amount = Amount::from_sat(42000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + ctx.next_block(); + ctx.next_conf(); + ctx.expect_credits(&credits); + }; + + println!("Without DB"); + { + ctx.stop_db(); + ctx.malformed_credit(&Amount::from_sat(24000)); + let metadata = rand_slice(); + let amount = Amount::from_sat(40000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + ctx.stop_node(); + ctx.expect_error(); + } + + println!("Reconnect DB"); + { + ctx.resume_db(); + ctx.resume_node(&[]); + let metadata = rand_slice(); + let amount = Amount::from_sat(2000); + ctx.debit(amount, metadata); + debits.push((metadata, amount)); + ctx.next_block(); + sleep(Duration::from_secs(3)); + ctx.next_block(); + sleep(Duration::from_secs(3)); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + } + + println!("Recover DB"); + { + let balance = ctx.wire_balance(); + ctx.reset_db(); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + } +} + +/// Test btc-wire ability to recover from errors in correctness critical paths and prevent concurrent sending +fn stress() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", true); + + let mut credits = Vec::new(); + let mut debits = Vec::new(); + + println!("Credit"); + { + let mut balance = ctx.wire_balance(); + for n in 10..30 { + let metadata = rand_slice(); + let amount = Amount::from_sat(n * 1000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + balance += amount; + ctx.next_block(); + } + ctx.next_conf(); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + }; + + println!("Debit"); + { + let mut balance = ctx.client_balance(); + for n in 10..30 { + let metadata = rand_slice(); + let amount = Amount::from_sat(n * 100); + balance += amount; + ctx.debit(amount, metadata); + debits.push((metadata, amount)); + } + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_client_balance(balance, true); + } + + println!("Bounce"); + { + ctx.reset_wallet(); + let mut balance = ctx.wire_balance(); + for n in 10..30 { + ctx.malformed_credit(&Amount::from_sat(n * 1000)); + balance += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(balance, true); + } + + println!("Recover DB"); + { + let balance = ctx.wire_balance(); + ctx.reset_db(); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + } +} + +/// Test btc-wire ability to handle conflicting outgoing transactions +fn conflict() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + println!("Conflict send"); + { + // Perform credit + let amount = Amount::from_sat(4200000); + ctx.credit(amount, rand_slice()); + ctx.next_conf(); + ctx.expect_wire_balance(amount, true); + let client = ctx.client_balance(); + let wire = ctx.wire_balance(); + + // Perform debit + ctx.debit(Amount::from_sat(400000), rand_slice()); + retry(|| ctx.wire_balance() < wire); + + // Abandon pending transaction + ctx.restart_node(&["-minrelaytxfee=0.0001"]); + ctx.abandon_wire(); + ctx.expect_client_balance(client, false); + ctx.expect_wire_balance(wire, false); + + // Generate conflict + ctx.debit(Amount::from_sat(500000), rand_slice()); + retry(|| ctx.wire_balance() < wire); + + // Resend conflicting transaction + ctx.restart_node(&[]); + ctx.next_block(); + let wire = ctx.wire_balance(); + retry(|| ctx.wire_balance() < wire); + } + + println!("Setup"); + drop(ctx); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + ctx.credit(Amount::from_sat(3000000), rand_slice()); + ctx.next_block(); + + println!("Conflict bounce"); + { + // Perform bounce + let wire = ctx.wire_balance(); + let bounce_amount = Amount::from_sat(4000000); + ctx.malformed_credit(&bounce_amount); + ctx.next_conf(); + let fee = ctx.state.bounce_fee; + ctx.expect_wire_balance(wire + fee, true); + + // Abandon pending transaction + ctx.restart_node(&["-minrelaytxfee=0.0001"]); + ctx.abandon_wire(); + ctx.expect_wire_balance(wire + bounce_amount, false); + + // Generate conflict + let amount = Amount::from_sat(50000); + ctx.debit(amount, rand_slice()); + retry(|| ctx.wire_balance() < (wire + bounce_amount)); + + // Resend conflicting transaction + ctx.restart_node(&[]); + let wire = ctx.wire_balance(); + ctx.next_block(); + retry(|| ctx.wire_balance() < wire); + } +} + +/// Test btc-wire correctness when a blockchain reorganization occurs +fn reorg() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + println!("Handle reorg incoming transactions"); + { + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform credits + let before = ctx.wire_balance(); + for n in 10..21 { + ctx.credit(Amount::from_sat(n * 10000), rand_slice()); + ctx.next_block(); + } + let after = ctx.wire_balance(); + + // Perform fork and check btc-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(22); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.mine(12); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + } + + println!("Handle reorg outgoing transactions"); + { + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform debits + let before = ctx.client_balance(); + let mut after = ctx.client_balance(); + for n in 10..21 { + let amount = Amount::from_sat(n * 100); + ctx.debit(amount, rand_slice()); + after += amount; + } + ctx.next_block(); + ctx.expect_client_balance(after, true); + + // Perform fork and check btc-wire still up + ctx.expect_gateway_up(); + ctx.cluster_fork(22); + ctx.expect_client_balance(before, false); + ctx.expect_gateway_up(); + + // Recover orphaned transaction + ctx.next_conf(); + ctx.expect_client_balance(after, false); + } + + println!("Handle reorg bounce"); + { + ctx.reset_wallet(); + + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform bounce + let before = ctx.wire_balance(); + let mut after = ctx.wire_balance(); + for n in 10..21 { + ctx.malformed_credit(&Amount::from_sat(n * 1000)); + after += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(after, true); + + // Perform fork and check btc-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(22); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.mine(10); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + } +} + +/// Test btc-wire correctness when a blockchain reorganization occurs leading to past incoming transaction conflict +fn hell() { + fn step(name: &str, action: impl FnOnce(&mut BtcCtx)) { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + println!("{}", name); + + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform action + action(&mut ctx); + + // Perform fork and check btc-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(ctx.conf * 2); + ctx.expect_gateway_down(); + + // Generate conflict + ctx.restart_node(&["-minrelaytxfee=0.001"]); + ctx.abandon_client(); + let amount = Amount::from_sat(54000); + ctx.credit(amount, rand_slice()); + ctx.expect_wire_balance(amount, true); + + // Check btc-wire suspend operation + let bounce_amount = Amount::from_sat(34000); + ctx.malformed_credit(&bounce_amount); + ctx.next_conf(); + ctx.expect_wire_balance(amount + bounce_amount, true); + ctx.expect_gateway_down(); + } + + step("Handle reorg conflicting incoming credit", |ctx| { + let amount = Amount::from_sat(420000); + ctx.credit(amount, rand_slice()); + ctx.next_conf(); + ctx.expect_wire_balance(amount, true); + }); + + step("Handle reorg conflicting incoming bounce", |ctx| { + let amount = Amount::from_sat(420000); + ctx.malformed_credit(&amount); + ctx.next_conf(); + let fee = ctx.state.bounce_fee; + ctx.expect_wire_balance(fee, true); + }); +} + +/// Test btc-wire ability to learn and protect itself from blockchain behavior +fn analysis() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + println!("Learn from reorg"); + + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform credit + let before = ctx.wire_balance(); + ctx.credit(Amount::from_sat(42000), rand_slice()); + ctx.next_conf(); + let after = ctx.wire_balance(); + + // Perform fork and check btc-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(5); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.next_conf(); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform credit + let before = ctx.wire_balance(); + ctx.credit(Amount::from_sat(42000), rand_slice()); + ctx.next_conf(); + + // Perform fork and check btc-wire learned from previous attack + ctx.expect_gateway_up(); + ctx.cluster_fork(5); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_up(); +} + +/// Test btc-wire ability to handle stuck transaction correctly +fn bumpfee() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc_bump.conf", false); + + // Perform credits to allow wire to perform debits latter + for n in 10..13 { + ctx.credit(Amount::from_sat(n * 100000), rand_slice()); + ctx.next_block(); + } + ctx.next_conf(); + + println!("Bump fee"); + { + // Perform debit + let mut client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let amount = Amount::from_sat(40000); + ctx.debit(amount, rand_slice()); + retry(|| ctx.wire_balance() < wire); + + // Bump min relay fee making the previous debit stuck + ctx.restart_node(&["-minrelaytxfee=0.0001"]); + + // Check bump happen + client += amount; + ctx.expect_client_balance(client, true); + } + + println!("Bump fee reorg"); + { + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform debit + let mut client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let amount = Amount::from_sat(40000); + ctx.debit(amount, rand_slice()); + retry(|| ctx.wire_balance() < wire); + + // Bump min relay fee and fork making the previous debit stuck and problematic + ctx.cluster_fork(6); + ctx.restart_node(&["-minrelaytxfee=0.0001"]); + + // Check bump happen + client += amount; + ctx.expect_client_balance(client, true); + } + + println!("Setup"); + drop(ctx); + let mut ctx = BtcCtx::setup("taler_btc_bump.conf", true); + + // Perform credits to allow wire to perform debits latter + for n in 10..61 { + ctx.credit(Amount::from_sat(n * 100000), rand_slice()); + ctx.next_block(); + } + ctx.next_conf(); + + println!("Bump fee stress"); + { + // Loose second bitcoin node + ctx.cluster_deco(); + + // Perform debits + let client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let mut total_amount = Amount::ZERO; + for n in 10..31 { + let amount = Amount::from_sat(n * 10000); + total_amount += amount; + ctx.debit(amount, rand_slice()); + } + retry(|| ctx.wire_balance() < wire - total_amount); + + // Bump min relay fee making the previous debits stuck + ctx.restart_node(&["-minrelaytxfee=0.0001"]); + + // Check bump happen + ctx.expect_client_balance(client + total_amount, true); + } +} + +/// Test btc-wire handle transaction fees exceeding limits +fn maxfee() { + println!("Setup"); + let mut ctx = BtcCtx::setup("taler_btc.conf", false); + + // Perform credits to allow wire to perform debits latter + for n in 10..31 { + ctx.credit(Amount::from_sat(n * 100000), rand_slice()); + ctx.next_block(); + } + ctx.next_conf(); + + let client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let mut total_amount = Amount::ZERO; + + println!("Too high fee"); + { + // Change fee config + ctx.restart_node(&["-maxtxfee=0.0000001", "-minrelaytxfee=0.0000001"]); + + // Perform debits + for n in 10..31 { + let amount = Amount::from_sat(n * 10000); + total_amount += amount; + ctx.debit(amount, rand_slice()); + } + sleep(Duration::from_secs(3)); + + // Check no transaction happen + ctx.expect_wire_balance(wire, true); + ctx.expect_client_balance(client, true); + } + + println!("Good feed"); + { + // Restore default config + ctx.restart_node(&[]); + + // Check transaction now have been made + ctx.expect_client_balance(client + total_amount, true); + } +} + +/// Test btc-wire ability to configure itself from bitcoin configuration +fn config() { + for n in 0..5 { + let config_name = format!("bitcoin_auth{}.conf", n); + println!("Config {}", config_name); + BtcCtx::config(&config_name); + } } diff --git a/instrumentation/src/eth.rs b/instrumentation/src/eth.rs @@ -1,14 +1,40 @@ -use std::{path::Path, time::Duration}; +/* + This file is part of TALER + Copyright (C) 2022 Taler Systems SA -use common::{metadata::OutMetadata, rand_slice}; + 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::{ + io::Write, + ops::{Deref, DerefMut}, + path::Path, + sync::atomic::Ordering, + thread::sleep, + time::Duration, +}; + +use common::{metadata::OutMetadata, postgres::NoTls, rand_slice}; use eth_wire::{ rpc::{hex::Hex, Rpc, RpcClient, TransactionRequest}, taler_util::{eth_payto_url, eth_to_taler, TRUNC}, RpcExtended, SyncState, WireState, }; -use ethereum_types::{H256, U256}; +use ethereum_types::{H160, H256, U256}; -use crate::{check_incoming, check_outgoing, print_now, transfer}; +use crate::utils::{ + check_incoming, check_outgoing, cmd_out, cmd_redirect, cmd_redirect_ok, print_now, retry, + retry_opt, transfer, ChildGuard, CommonCtx, +}; fn wait_for_pending(rpc: &mut Rpc) { print_now("Wait for pending transactions mining:"); @@ -19,7 +45,7 @@ fn wait_for_pending(rpc: &mut Rpc) { print_now("."); std::thread::sleep(Duration::from_secs(1)); // Wait for eth-wire to act } - println!(""); + println!(); } pub fn eth_test(config: Option<&Path>, base_url: &str) { @@ -41,7 +67,7 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { .unwrap_or_else(|| rpc.new_account("password").unwrap()); // Else create account rpc.unlock_account(&client_addr, "password").unwrap(); - if rpc.get_balance(&client_addr).unwrap() < min_fund { + if rpc.get_balance_latest(&client_addr).unwrap() < min_fund { println!( "Client need a minimum of {} WEI to run this test, send coins to this address: {}", min_fund.as_u64(), @@ -49,17 +75,17 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { ); print_now("Waiting for fund:"); let mut rpc = rpc.subscribe_new_head().unwrap(); - while rpc.get_balance(&client_addr).unwrap() < min_fund { + while rpc.get_balance_latest(&client_addr).unwrap() < min_fund { rpc.next().unwrap(); print_now("."); } - println!(""); + println!(); } wait_for_pending(&mut rpc); // Load balances - let client_balance = rpc.get_balance(&client_addr).unwrap(); - let wire_balance = rpc.get_balance(&state.address).unwrap(); + let client_balance = rpc.get_balance_latest(&client_addr).unwrap(); + let wire_balance = rpc.get_balance_latest(&state.address).unwrap(); // Start sync state let latest = rpc.latest_block().unwrap(); let mut sync_state = SyncState { @@ -120,12 +146,12 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { print_now("."); } }; - println!(""); + println!(); wait_for_pending(&mut rpc); println!("Check balance"); - let new_client_balance = rpc.get_balance(&client_addr).unwrap(); - let new_wire_balance = rpc.get_balance(&state.address).unwrap(); + let new_client_balance = rpc.get_balance_latest(&client_addr).unwrap(); + let new_wire_balance = rpc.get_balance_latest(&state.address).unwrap(); let client_sent_amount_cost = test_amount * U256::from(2u8); let client_sent_fees_cost = [credit_id, zero_id, bounce_id] .into_iter() @@ -146,8 +172,13 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { new_wire_balance ); - check_incoming(base_url, &reserve_pub_key, &taler_test_amount); + println!("Check incoming history"); + assert!(check_incoming( + base_url, + &[(reserve_pub_key, taler_test_amount.clone())] + )); + println!("Get back some money"); let wtid = rand_slice(); transfer( base_url, @@ -159,8 +190,935 @@ pub fn eth_test(config: Option<&Path>, base_url: &str) { wait_for_pending(&mut rpc); println!("Check balances"); - let last_client_balance = rpc.get_balance(&client_addr).unwrap(); + let last_client_balance = rpc.get_balance_latest(&client_addr).unwrap(); assert_eq!(new_client_balance + test_amount, last_client_balance); - check_outgoing(base_url, &wtid, &state.base_url, &taler_test_amount); + println!("Check outgoing history"); + assert!(check_outgoing( + base_url, + &state.base_url, + &[(wtid, taler_test_amount)] + )); +} + +struct EthCtx { + node: ChildGuard, + rpc: Rpc, + wire_addr: H160, + client_addr: H160, + reserve_addr: H160, + state: WireState, + conf: u16, + common: CommonCtx, + passwd: String, +} + +impl Deref for EthCtx { + type Target = CommonCtx; + + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl DerefMut for EthCtx { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +impl EthCtx { + pub fn setup(config: &str, stressed: bool) -> Self { + let dirs = CommonCtx::setup_dirs(config); + // Init chain + let passwd = std::env::var("PASSWORD").unwrap(); + let pswd_path = dirs.dir.path().join("pswd"); + std::fs::write(&pswd_path, passwd.as_bytes()).unwrap(); + for _ in ["reserve", "client"] { + cmd_redirect_ok( + "geth", + &[ + "--datadir", + dirs.wire_dir.to_str().unwrap(), + "account", + "new", + "--password", + pswd_path.to_str().unwrap(), + ], + "log/node.log", + "create account", + ) + } + cmd_redirect_ok( + "geth", + &[ + "--datadir", + dirs.wire_dir2.to_str().unwrap(), + "account", + "new", + "--password", + pswd_path.to_str().unwrap(), + ], + "log/node2.log", + "create account", + ); + let list = cmd_out( + "geth", + &[ + "--datadir", + dirs.wire_dir.to_str().unwrap(), + "account", + "list", + ], + ); + let addr = &list.lines().nth(1).unwrap()[13..][..40]; + let genesis = format!( + "{{ + \"config\": {{ + \"chainId\": 42, + \"homesteadBlock\": 0, + \"eip150Block\": 0, + \"eip155Block\": 0, + \"eip158Block\": 0, + \"byzantiumBlock\": 0, + \"constantinopleBlock\": 0, + \"petersburgBlock\": 0, + \"istanbulBlock\": 0, + \"berlinBlock\": 0, + \"londonBlock:\": 0, + \"ethash\": {{}} + }}, + \"difficulty\": \"1\", + \"gasLimit\": \"0\", + \"baseFeePerGas\": null, + \"alloc\": {{ + \"{}\": {{ \"balance\": \"10000000000000000000\" }} + }} + }}", + addr + ); + std::fs::write(dirs.wire_dir.join("genesis.json"), genesis.as_bytes()).unwrap(); + + cmd_redirect_ok( + "geth", + &[ + "--datadir", + dirs.wire_dir.to_str().unwrap(), + "init", + dirs.wire_dir.join("genesis.json").to_str().unwrap(), + ], + "log/node.log", + "init chain", + ); + cmd_redirect_ok( + "geth", + &[ + "--datadir", + dirs.wire_dir2.to_str().unwrap(), + "init", + dirs.wire_dir.join("genesis.json").to_str().unwrap(), + ], + "log/node2.log", + "init chain2", + ); + + let node = cmd_redirect( + "geth", + &[ + "--datadir", + dirs.wire_dir.to_str().unwrap(), + "--miner.gasprice", + "10", + ], + "log/node.log", + ); + let mut rpc = retry_opt(|| Rpc::new(&dirs.wire_dir).ok()); + + let common = CommonCtx::setup(dirs, "eth-wire", stressed, |dirs| { + // Generate wallet + let out = cmd_out( + "eth-wire", + &["-c", dirs.conf.to_str().unwrap(), "initwallet"], + ); + let payto = format!("\n{}", out.lines().nth(6).unwrap()); + std::fs::OpenOptions::new() + .append(true) + .open(&dirs.conf) + .unwrap() + .write_all(payto.as_bytes()) + .unwrap(); + }); + let state = WireState::load_taler_config(Some(&common.dirs.conf)); + let accounts = rpc.list_accounts().unwrap(); + let reserve_addr = accounts[0]; + let client_addr = accounts[1]; + let wire_addr = accounts[2]; + for addr in [&client_addr, &reserve_addr] { + rpc.unlock_account(addr, &passwd).unwrap(); + } + let conf = state.confirmation.load(Ordering::SeqCst) as u16; + + Self { + node, + rpc, + reserve_addr, + client_addr, + wire_addr, + state, + common, + conf, + passwd, + } + } + + pub fn reset_db(&mut self) { + let block = self.rpc.earliest_block().unwrap(); + let mut db = self.common.taler_conf.db_config().connect(NoTls).unwrap(); + let mut tx = db.transaction().unwrap(); + // Clear transaction tables and reset state + tx.batch_execute("DELETE FROM tx_in;DELETE FROM tx_out;DELETE FROM bounce;") + .unwrap(); + tx.execute( + "UPDATE state SET value=$1 WHERE name='sync'", + &[&SyncState { + tip_hash: block.hash.unwrap(), + tip_height: block.number.unwrap(), + conf_height: block.number.unwrap(), + } + .to_bytes() + .as_ref()], + ) + .unwrap(); + tx.commit().unwrap(); + } + + pub fn stop_node(&mut self) { + // We need to kill node gracefully to avoid corruption + #[cfg(unix)] + { + cmd_redirect_ok( + "kill", + &[&self.node.0.id().to_string()], + "/dev/null", + "fill btc node", + ); + self.node.0.wait().unwrap(); + } + } + + // We use the import/export chain functionality to simulate a connected node peer + // because local network peer are not reliable + // importChain RPC crash so we have to use the cli for now + + fn export(rpc: &mut Rpc, path: &str) { + std::fs::remove_file(path).ok(); + assert!(rpc.export_chain(path).unwrap()) + } + + pub fn cluster_deco(&mut self) { + let path = self.dirs.dir.path().join("chain"); + let path = path.to_str().unwrap(); + Self::export(&mut self.rpc, path); + cmd_redirect_ok( + "geth", + &[ + "--datadir", + self.dirs.wire_dir2.to_str().unwrap(), + "import", + path, + ], + "log/node2.log", + "import chain", + ); + } + + pub fn cluster_fork(&mut self, length: u16) { + let node2 = cmd_redirect( + "geth", + &[ + "--datadir", + self.dirs.wire_dir2.to_str().unwrap(), + "--port", + "30305", + "--miner.gasprice", + "10", + ], + "log/node2.log", + ); + let mut rpc = retry_opt(|| Rpc::new(&self.dirs.wire_dir2).ok()); + Self::_mine(&mut rpc, &self.reserve_addr, length, &self.passwd); + let path = self.dirs.dir.path().join("chain"); + let path = path.to_str().unwrap(); + Self::export(&mut rpc, path); + drop(node2); + self.stop_node(); + cmd_redirect_ok( + "geth", + &[ + "--datadir", + self.dirs.wire_dir.to_str().unwrap(), + "import", + path, + ], + "log/node.log", + "import chain", + ); + self.resume_node(&[]); + } + + pub fn restart_node(&mut self, additional_args: &[&str]) { + self.stop_node(); + self.resume_node(additional_args); + } + + pub fn resume_node(&mut self, additional_args: &[&str]) { + let datadir = format!("-datadir={}", self.dirs.wire_dir.to_string_lossy()); + let mut args = vec![datadir.as_str()]; + args.extend_from_slice(additional_args); + self.node = cmd_redirect( + "geth", + &["--datadir", self.dirs.wire_dir.to_str().unwrap()], + "log/node.log", + ); + self.rpc = retry_opt(|| Rpc::new(&self.dirs.wire_dir).ok()); + for addr in [&self.wire_addr, &self.client_addr, &self.reserve_addr] { + self.rpc.unlock_account(addr, &self.passwd).unwrap(); + } + } + + pub fn amount(&self, amount: u32) -> U256 { + return U256::from(amount) * TRUNC; + } + + /* ----- Transaction ------ */ + + pub fn credit(&mut self, amount: U256, metadata: [u8; 32]) { + self.rpc + .credit(self.client_addr, self.wire_addr, amount, metadata) + .unwrap(); + } + + pub fn debit(&mut self, amount: U256, metadata: [u8; 32]) { + transfer( + &self.common.gateway_url, + &metadata, + &self.state.base_url, + eth_payto_url(&self.client_addr), + &eth_to_taler(&amount, self.state.currency), + ) + } + + pub fn malformed_credit(&mut self, amount: U256) { + self.rpc + .send_transaction(&TransactionRequest { + from: self.client_addr, + to: self.wire_addr, + value: amount, + nonce: None, + gas_price: None, + data: Hex(vec![]), + }) + .unwrap(); + } + + pub fn abandon(&mut self) { + let pending = self.rpc.pending_transactions().unwrap(); + for tx in pending + .into_iter() + .filter(|t| t.from == Some(self.client_addr)) + { + // Replace transaction value with 0 + self.rpc + .send_transaction(&TransactionRequest { + from: self.client_addr, + to: tx.to.unwrap(), + value: U256::zero(), + gas_price: Some(U256::from(110)), // Bigger gas price to replace fee + data: Hex(vec![]), + nonce: Some(tx.nonce), + }) + .unwrap(); + } + } + + /* ----- Mining ----- */ + + fn _mine(rpc: &mut Rpc, addr: &H160, mut amount: u16, passwd: &str) { + rpc.unlock_account(addr, passwd).ok(); + let mut rpc = rpc.subscribe_new_head().unwrap(); + + rpc.miner_start().unwrap(); + while !rpc.pending_transactions().unwrap().is_empty() { + rpc.next().unwrap(); + amount = amount.saturating_sub(1); + } + for _ in 0..amount { + rpc.next().unwrap(); + } + rpc.miner_stop().unwrap(); + } + + fn mine(&mut self, nb: u16) { + Self::_mine(&mut self.rpc, &self.reserve_addr, nb, &self.passwd) + } + + pub fn next_conf(&mut self) { + self.mine(self.conf) + } + + pub fn next_block(&mut self) { + self.mine(1) + } + + /* ----- Balances ----- */ + + pub fn client_balance(&mut self) -> U256 { + self.rpc.get_balance_latest(&self.client_addr).unwrap() + } + + pub fn wire_balance(&mut self) -> U256 { + self.rpc.get_balance_latest(&self.wire_addr).unwrap() + } + + pub fn wire_balance_pending(&mut self) -> U256 { + self.rpc.get_balance_pending(&self.wire_addr).unwrap() + } + + fn expect_balance(&mut self, balance: U256, mine: bool, lambda: fn(&mut Self) -> U256) { + retry(|| { + let check = balance == lambda(self); + if !check && mine { + self.next_block(); + } + check + }); + } + + pub fn expect_client_balance(&mut self, balance: U256, mine: bool) { + self.expect_balance(balance, mine, Self::client_balance) + } + + pub fn expect_wire_balance(&mut self, balance: U256, mine: bool) { + self.expect_balance(balance, mine, Self::wire_balance) + } + + /* ----- Wire Gateway ----- */ + + pub fn expect_credits(&self, txs: &[([u8; 32], U256)]) { + let txs: Vec<_> = txs + .iter() + .map(|(metadata, amount)| (*metadata, eth_to_taler(amount, self.state.currency))) + .collect(); + self.common.expect_credits(&txs) + } + + pub fn expect_debits(&self, txs: &[([u8; 32], U256)]) { + let txs: Vec<_> = txs + .iter() + .map(|(metadata, amount)| (*metadata, eth_to_taler(amount, self.state.currency))) + .collect(); + self.common.expect_debits(&self.state.base_url, &txs) + } +} + +pub const TESTS: &[(fn(), &str)] = &[ + (wire, "eth_wire"), + (lifetime, "eth_lifetime"), + (reconnect, "eth_reconnect"), + (stress, "eth_stress"), + (reorg, "eth_reorg"), + (hell, "eth_hell"), + (analysis, "eth_analysis"), + (bumpfee, "eth_bumpfee"), + (maxfee, "eth_maxfee"), +]; + +/// Test eth-wire correctly receive and send transactions on the blockchain +fn wire() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + + println!("Credit"); + { + // Send transactions + let mut balance = ctx.wire_balance(); + let mut txs = Vec::new(); + for n in 10..100 { + let metadata = rand_slice(); + let amount = ctx.amount(n * 1000); + ctx.credit(amount, metadata); + txs.push((metadata, amount)); + balance += amount; + } + ctx.next_conf(); + ctx.expect_credits(&txs); + ctx.expect_wire_balance(balance, true); + }; + + println!("Debit"); + { + let mut balance = ctx.client_balance(); + let mut txs = Vec::new(); + for n in 10..100 { + let metadata = rand_slice(); + let amount = ctx.amount(n * 100); + balance += amount; + ctx.debit(amount, metadata); + txs.push((metadata, amount)); + } + ctx.next_block(); + ctx.expect_debits(&txs); + ctx.expect_client_balance(balance, true); + } + + println!("Bounce"); + { + // Send bad transactions + let mut balance = ctx.wire_balance(); + for n in 10..40 { + ctx.malformed_credit(ctx.amount(n * 1000)); + balance += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(balance, true); + } +} + +/// Test eth-wire and wire-gateway correctly stop when a lifetime limit is configured +fn lifetime() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth_lifetime.conf", false); + println!("Check lifetime"); + // Start up + retry(|| ctx.wire_running() && ctx.gateway_running()); + // Consume lifetime + for n in 0..=ctx.taler_conf.wire_lifetime().unwrap() { + ctx.credit(ctx.amount(n * 1000), rand_slice()); + ctx.next_block(); + } + for n in 0..=ctx.taler_conf.http_lifetime().unwrap() { + ctx.debit(ctx.amount(n * 1000), rand_slice()); + } + // End down + retry(|| !ctx.wire_running() && !ctx.gateway_running()); +} + +/// Check the capacity of wire-gateway and eth-wire to recover from database and node loss +fn reconnect() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + + let mut credits = Vec::new(); + let mut debits = Vec::new(); + + println!("With DB"); + { + let metadata = rand_slice(); + let amount = ctx.amount(42000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + ctx.next_block(); + ctx.next_conf(); + ctx.expect_credits(&credits); + }; + + println!("Without DB"); + { + ctx.stop_db(); + ctx.malformed_credit(ctx.amount(24000)); + let metadata = rand_slice(); + let amount = ctx.amount(4000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + ctx.stop_node(); + ctx.expect_error(); + } + + println!("Reconnect DB"); + { + ctx.resume_db(); + ctx.resume_node(&[]); + let metadata = rand_slice(); + let amount = ctx.amount(2000); + ctx.debit(amount, metadata); + debits.push((metadata, amount)); + ctx.next_block(); + sleep(Duration::from_secs(3)); + ctx.next_block(); + sleep(Duration::from_secs(3)); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + } + + println!("Recover DB"); + { + ctx.next_block(); + sleep(Duration::from_secs(3)); + ctx.next_block(); + sleep(Duration::from_secs(3)); + let balance = ctx.wire_balance(); + ctx.reset_db(); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + } +} + +/// Test eth-wire ability to recover from errors in correctness critical paths and prevent concurrent sending +fn stress() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", true); + + let mut credits = Vec::new(); + let mut debits = Vec::new(); + + println!("Credit"); + { + let mut balance = ctx.wire_balance(); + for n in 10..30 { + let metadata = rand_slice(); + let amount = ctx.amount(n * 1000); + ctx.credit(amount, metadata); + credits.push((metadata, amount)); + balance += amount; + } + ctx.next_conf(); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + }; + + println!("Debit"); + { + let mut balance = ctx.client_balance(); + for n in 10..30 { + let metadata = rand_slice(); + let amount = ctx.amount(n * 100); + balance += amount; + ctx.debit(amount, metadata); + debits.push((metadata, amount)); + } + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_client_balance(balance, true); + } + + println!("Bounce"); + { + let mut balance = ctx.wire_balance(); + for n in 10..30 { + ctx.malformed_credit(ctx.amount(n * 1000)); + balance += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(balance, true); + } + + println!("Recover DB"); + { + let balance = ctx.wire_balance(); + ctx.reset_db(); + ctx.next_block(); + ctx.expect_debits(&debits); + ctx.expect_credits(&credits); + ctx.expect_wire_balance(balance, true); + } +} + +/// Test eth-wire correctness when a blockchain reorganization occurs +fn reorg() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + + println!("Handle reorg incoming transactions"); + { + // Loose second node + ctx.cluster_deco(); + + // Perform credits + let before = ctx.wire_balance(); + for n in 10..21 { + ctx.credit(ctx.amount(n * 10000), rand_slice()); + } + ctx.next_conf(); + let after = ctx.wire_balance(); + + // Perform fork and check eth-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(10); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.mine(6); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + } + + println!("Handle reorg outgoing transactions"); + { + // Loose second node + ctx.cluster_deco(); + + // Perform debits + let before = ctx.client_balance(); + let mut after = ctx.client_balance(); + for n in 10..21 { + let amount = ctx.amount(n * 100); + ctx.debit(amount, rand_slice()); + after += amount; + } + ctx.next_block(); + ctx.expect_client_balance(after, true); + + // Perform fork and check eth-wire still up + ctx.expect_gateway_up(); + ctx.cluster_fork(10); + ctx.expect_client_balance(before, false); + ctx.expect_gateway_up(); + + // Recover orphaned transaction + ctx.next_conf(); + ctx.expect_client_balance(after, false); + } + + println!("Handle reorg bounce"); + { + // Loose second node + ctx.cluster_deco(); + + // Perform bounce + let before = ctx.wire_balance(); + let mut after = ctx.wire_balance(); + for n in 10..21 { + ctx.malformed_credit(ctx.amount(n * 1000)); + after += ctx.state.bounce_fee; + } + ctx.next_conf(); + ctx.expect_wire_balance(after, true); + + // Perform fork and check eth-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(10); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.mine(10); + sleep(Duration::from_secs(3)); + ctx.next_block(); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + } +} + +/// Test eth-wire correctness when a blockchain reorganization occurs leading to past incoming transaction conflict +fn hell() { + fn step(name: &str, action: impl FnOnce(&mut EthCtx)) { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + println!("{}", name); + + // Loose second node + ctx.cluster_deco(); + + // Perform action + action(&mut ctx); + + // Perform fork and check eth-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(ctx.conf * 2); + ctx.expect_gateway_down(); + + // Generate conflict + ctx.restart_node(&["--miner.gasprice", "1000"]); + ctx.abandon(); + let amount = ctx.amount(54000); + ctx.credit(amount, rand_slice()); + ctx.expect_wire_balance(amount, true); + + // Check eth-wire suspend operation + let bounce_amount = ctx.amount(34000); + ctx.malformed_credit(bounce_amount); + ctx.next_conf(); + ctx.expect_wire_balance(amount + bounce_amount, true); + ctx.expect_gateway_down(); + } + + step("Handle reorg conflicting incoming credit", |ctx| { + let amount = ctx.amount(420000); + ctx.credit(amount, rand_slice()); + ctx.next_conf(); + ctx.expect_wire_balance(amount, true); + }); + + step("Handle reorg conflicting incoming bounce", |ctx| { + let amount = ctx.amount(420000); + ctx.malformed_credit(amount); + ctx.next_conf(); + retry(|| ctx.wire_balance_pending() == ctx.state.bounce_fee); + }); +} + +/// Test eth-wire ability to learn and protect itself from blockchain behavior +fn analysis() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + + println!("Learn from reorg"); + + // Loose second node + ctx.cluster_deco(); + + // Perform credit + let before = ctx.wire_balance(); + ctx.credit(ctx.amount(42000), rand_slice()); + ctx.next_conf(); + let after = ctx.wire_balance(); + + // Perform fork and check eth-wire hard error + ctx.expect_gateway_up(); + ctx.cluster_fork(6); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_down(); + + // Recover orphaned transaction + ctx.mine(6); + ctx.expect_wire_balance(after, false); + ctx.expect_gateway_up(); + + // Loose second node + ctx.cluster_deco(); + + // Perform credit + let before = ctx.wire_balance(); + ctx.credit(ctx.amount(42000), rand_slice()); + ctx.next_conf(); + + // Perform fork and check eth-wire learned from previous attack + ctx.expect_gateway_up(); + ctx.cluster_fork(5); + ctx.expect_wire_balance(before, false); + ctx.expect_gateway_up(); +} + +/// Test eth-wire ability to handle stuck transaction correctly +fn bumpfee() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth_bump.conf", false); + + // Perform credits to allow wire to perform debits latter + ctx.credit(ctx.amount(90000000), rand_slice()); + ctx.next_conf(); + + println!("Bump fee"); + { + // Perform debit + let mut client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let amount = ctx.amount(40000); + ctx.debit(amount, rand_slice()); + retry(|| ctx.wire_balance_pending() < wire); + + // Bump min relay fee making the previous debit stuck + ctx.restart_node(&["--miner.gasprice", "1000"]); + + // Check bump happen + client += amount; + ctx.expect_client_balance(client, true); + } + + println!("Bump fee reorg"); + { + // Loose second node + ctx.cluster_deco(); + + // Perform debit + let mut client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let amount = ctx.amount(40000); + ctx.debit(amount, rand_slice()); + retry(|| ctx.wire_balance_pending() < wire); + + // Bump min relay fee and fork making the previous debit stuck and problematic + ctx.cluster_fork(6); + ctx.restart_node(&["--miner.gasprice", "2000"]); + + // Check bump happen + client += amount; + ctx.expect_client_balance(client, true); + } + + println!("Setup"); + drop(ctx); + let mut ctx = EthCtx::setup("taler_eth_bump.conf", true); + + // Perform credit to allow wire to perform debits latter + ctx.credit(ctx.amount(9000000), rand_slice()); + ctx.next_conf(); + + println!("Bump fee stress"); + { + // Loose second node + ctx.cluster_deco(); + + // Perform debits + let client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let mut total_amount = U256::zero(); + for n in 10..31 { + let amount = ctx.amount(n * 10000); + total_amount += amount; + ctx.debit(amount, rand_slice()); + } + retry(|| ctx.wire_balance_pending() < wire - total_amount); + + // Bump min relay fee making the previous debits stuck + ctx.restart_node(&["--miner.gasprice", "1000"]); + + // Check bump happen + ctx.expect_client_balance(client + total_amount, true); + } +} + +/// Test eth-wire handle transaction fees exceeding limits +fn maxfee() { + println!("Setup"); + let mut ctx = EthCtx::setup("taler_eth.conf", false); + + // Perform credit to allow wire to perform debits latter + ctx.credit(ctx.amount(9000000), rand_slice()); + ctx.next_conf(); + + let client = ctx.client_balance(); + let wire = ctx.wire_balance(); + let mut total_amount = U256::zero(); + + println!("Too high fee"); + { + // Change fee config + ctx.restart_node(&["--rpc.txfeecap", "0.00001"]); + + // Perform debits + for n in 10..31 { + let amount = ctx.amount(n * 10000); + total_amount += amount; + ctx.debit(amount, rand_slice()); + } + sleep(Duration::from_secs(3)); + + // Check no transaction happen + ctx.expect_wire_balance(wire, true); + ctx.expect_client_balance(client, true); + } + + println!("Good feed"); + { + // Restore default config + ctx.restart_node(&[""]); + + // Check transaction now have been made + ctx.expect_client_balance(client + total_amount, true); + } } diff --git a/instrumentation/src/gateway.rs b/instrumentation/src/gateway.rs @@ -0,0 +1,244 @@ +/* + 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::str::FromStr; + +use btc_wire::taler_utils::btc_payto_url; +use common::{ + api_common::{Amount as TalerAmount, Base32}, + api_wire::TransferRequest, + rand_slice, + url::Url, +}; +use libdeflater::{CompressionLvl, Compressor}; +use ureq::Response; + +use crate::{ + btc::BtcCtx, + utils::{cmd_out, cmd_redirect_ok, gateway_error}, +}; + +pub const TESTS: &[(fn(), &str)] = &[(api, "api"), (auth, "auth")]; + +fn client_transfer(gateway_url: &str, payto_url: &str, amount: &str) -> String { + cmd_out( + "taler-exchange-wire-gateway-client", + &["-b", gateway_url, "-C", payto_url, "-a", amount], + ) +} + +fn http_code(response: Result<Response, ureq::Error>) -> u16 { + match response { + Ok(resp) => resp.status(), + Err(err) => match err { + ureq::Error::Status(err, _) => err, + ureq::Error::Transport(_) => unreachable!(), + }, + } +} + +/// Test wire-gateway conformance to documentation and its security +fn api() { + println!("Setup"); + let ctx = BtcCtx::setup("taler_btc.conf", false); + + println!("Gateway API"); + { + // Perform debits + let mut amounts = Vec::new(); + for n in 1..10 { + let amount = format!("{}:0.000{}", ctx.taler_conf.currency.to_str(), n); + cmd_out( + "taler-exchange-wire-gateway-client", + &[ + "-b", + &ctx.gateway_url, + "-D", + &btc_payto_url(&ctx.client_addr).to_string(), + "-a", + &amount, + ], + ); + amounts.push(amount); + } + + // Check history + let result = cmd_out( + "taler-exchange-wire-gateway-client", + &["-b", &ctx.gateway_url, "-i"], + ); + for amount in &amounts { + assert!(result.contains(amount)); + } + + // Perform credits + let mut amounts = Vec::new(); + for n in 1..10 { + let amount = format!("{}:0.0000{}", ctx.taler_conf.currency.to_str(), n); + client_transfer( + &ctx.gateway_url, + &btc_payto_url(&ctx.client_addr).to_string(), + &amount, + ); + amounts.push(amount); + } + + // Check history + let result = cmd_out( + "taler-exchange-wire-gateway-client", + &["-b", &ctx.gateway_url, "-o"], + ); + for amount in &amounts { + assert!(result.contains(amount)); + } + }; + + println!("Endpoint & Method"); + { + // Unknown endpoint + gateway_error(&format!("{}test", ctx.gateway_url), 404); + // Method not allowed + gateway_error(&format!("{}transfer", ctx.gateway_url), 405); + } + + let amount = &format!("{}:0.00042", ctx.taler_conf.currency.to_str()); + let payto = btc_payto_url(&ctx.client_addr).to_string(); + + println!("Request format"); + { + // Bad payto_url + for url in [ + "http://bitcoin/$CLIENT", + "payto://btc/$CLIENT", + "payto://bitcoin/$CLIENT?id=admin", + "payto://bitcoin/$CLIENT#admin", + "payto://bitcoin/42$CLIENT", + ] { + let url = url.replace("$CLIENT", &ctx.client_addr.to_string()); + let result = client_transfer(&ctx.gateway_url, &url, amount); + assert!(result.contains("(400/24)")); + } + + // Bad transaction amount + let result = client_transfer(&ctx.gateway_url, &payto, "ATC:0.00042"); + assert!(result.contains("(400/26)")); + + // Bad history delta + for delta in [ + "incoming", + "outgoing", + "incoming?delta=0", + "outgoing?delta=0;", + ] { + let code = + http_code(ureq::get(&format!("{}history/{}", ctx.gateway_url, delta)).call()); + assert_eq!(code, 400); + } + } + + println!("Transfer idempotence"); + { + let mut request = TransferRequest { + request_uid: Base32::from(rand_slice()), + amount: TalerAmount::from_str(amount).unwrap(), + exchange_base_url: ctx.taler_conf.base_url(), + wtid: Base32::from(rand_slice()), + credit_account: Url::from_str(&payto).unwrap(), + }; + // Same + assert_eq!( + http_code(ureq::post(&format!("{}transfer", ctx.gateway_url)).send_json(&request)), + 200 + ); + assert_eq!( + http_code(ureq::post(&format!("{}transfer", ctx.gateway_url)).send_json(&request)), + 200 + ); + // Collision + request.amount.fraction += 42; + assert_eq!( + http_code(ureq::post(&format!("{}transfer", ctx.gateway_url)).send_json(&request)), + 409 + ); + } + + println!("Security"); + { + let big_hello: String = (0..1000).map(|_| "Hello_world").collect(); + // Huge body + assert_eq!( + http_code(ureq::post(&format!("{}transfer", ctx.gateway_url)).send_json(&big_hello)), + 400 + ); + + // Body length liar + assert_eq!( + http_code( + ureq::post(&format!("{}transfer", ctx.gateway_url)) + .set("Content-Length", "1024") + .send_json(&big_hello) + ), + 400 + ); + + // Compression bomb + let mut compressor = Compressor::new(CompressionLvl::best()); + let mut compressed = Vec::new(); + compressed.resize(compressor.deflate_compress_bound(big_hello.len()), 0); + let size = compressor + .deflate_compress(big_hello.as_bytes(), &mut compressed) + .unwrap(); + compressed.resize(size, 0); + assert_eq!( + http_code( + ureq::post(&format!("{}transfer", ctx.gateway_url)) + .set("Content-Encoding", "deflate") + .send_bytes(&compressed) + ), + 400 + ); + } +} + +/// Check btc-wire and wire-gateway correctly stop when a lifetime limit is configured +fn auth() { + println!("Setup"); + let ctx = BtcCtx::setup("taler_btc_auth.conf", false); + + println!("Authentication"); + // No auth + assert_eq!( + http_code(ureq::get(&format!("{}history.outgoing", ctx.gateway_url)).call()), + 401 + ); + + // Auth + cmd_redirect_ok( + "taler-exchange-wire-gateway-client", + &[ + "--config", + ctx.dirs.conf.to_str().unwrap(), + "-s", + "exchange-accountcredentials-admin", + "-C", + &btc_payto_url(&ctx.client_addr).to_string(), + "-a", + &format!("{}:0.00042", ctx.taler_conf.currency.to_str()), + ], + "log/client.log", + "", + ); +} diff --git a/instrumentation/src/main.rs b/instrumentation/src/main.rs @@ -14,92 +14,71 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, io::Write, path::PathBuf}; +use std::{path::PathBuf, time::Instant}; use btc::btc_test; use clap::StructOpt; -use common::{ - api_common::{Amount, Base32}, - api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, - config::TalerConfig, - currency::Currency, - rand_slice, - url::Url, -}; +use common::{config::TalerConfig, currency::Currency}; use eth::eth_test; +use owo_colors::OwoColorize; mod btc; mod eth; - -fn print_now(disp: impl Display) { - print!("{}", disp); - std::io::stdout().flush().unwrap(); -} - -fn check_incoming(base_url: &str, reserve_pub_key: &[u8; 32], taler_amount: &Amount) { - println!("Check incoming history"); - let history: IncomingHistory = ureq::get(&format!("{}/history/incoming", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history.incoming_transactions.iter().any(|h| { - matches!( - h, - IncomingBankTransaction::IncomingReserveTransaction { - reserve_pub, - amount, - .. - } if reserve_pub == &Base32::from(*reserve_pub_key) && amount == taler_amount - ) - })); -} - -fn transfer(base_url: &str, wtid: &[u8; 32], url: &Url, credit_account: Url, amount: &Amount) { - println!("Get back some money"); - ureq::post(&format!("{}/transfer", base_url)) - .send_json(TransferRequest { - request_uid: Base32::from(rand_slice()), - amount: amount.clone(), - exchange_base_url: url.clone(), - wtid: Base32::from(*wtid), - credit_account, - }) - .unwrap(); -} - -fn check_outgoing(base_url: &str, wtid: &[u8; 32], url: &Url, amount: &Amount) { - println!("Check outgoing history"); - let history: OutgoingHistory = ureq::get(&format!("{}/history/outgoing", base_url)) - .query("delta", "-5") - .call() - .unwrap() - .into_json() - .unwrap(); - assert!(history.outgoing_transactions.iter().any(|h| { - h.wtid == Base32::from(*wtid) && &h.exchange_base_url == url && &h.amount == amount - })); -} +mod gateway; +mod utils; /// Depolymerizer instrumentation test #[derive(clap::Parser, Debug)] struct Args { - /// Override default configuration file path - #[clap(global = true, short, long)] - config: Option<PathBuf>, + #[clap(subcommand)] + cmd: Cmd, +} + +#[derive(clap::Subcommand, Debug)] +enum Cmd { + /// Perform online tests on running blockchain + Online { + /// Override default configuration file path + #[clap(global = true, short, long)] + config: Option<PathBuf>, + }, + /// Perform offline tests on local private blockchain + Offline, } pub fn main() { common::log::init(); + color_backtrace::install(); + let args = Args::parse(); - let taler_config = TalerConfig::load(args.config.as_deref()); - let base_url = format!("http://localhost:{}", taler_config.port()); + match args.cmd { + Cmd::Online { config } => { + let taler_config = TalerConfig::load(config.as_deref()); + let base_url = format!("http://localhost:{}", taler_config.port()); - match taler_config.currency { - Currency::BTC(_) => btc_test(args.config.as_deref(), &base_url), - Currency::ETH(_) => eth_test(args.config.as_deref(), &base_url), + match taler_config.currency { + Currency::BTC(_) => btc_test(config.as_deref(), &base_url), + Currency::ETH(_) => eth_test(config.as_deref(), &base_url), + } + println!("Instrumentation test successful"); + } + Cmd::Offline => { + let mut tests = Vec::new(); + tests.extend_from_slice(gateway::TESTS); + tests.extend_from_slice(btc::TESTS); + tests.extend_from_slice(eth::TESTS); + let start = Instant::now(); + for (test, name) in &tests { + let start = Instant::now(); + println!("{}", name.magenta()); + test(); + println!( + "{} {}", + "OK".green(), + format_args!("{:?}", start.elapsed()).bright_black() + ); + } + println!("{} tests in {:?}", tests.len(), start.elapsed()); + } } - - println!("Instrumentation test successful"); } diff --git a/instrumentation/src/utils.rs b/instrumentation/src/utils.rs @@ -0,0 +1,444 @@ +/* + 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::{ + fmt::Display, + fmt::Write as _, + io::Write as _, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + str::FromStr, + thread::sleep, + time::{Duration, Instant}, +}; + +use common::{ + api_common::{Amount, Base32}, + api_wire::{IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferRequest}, + config::TalerConfig, + rand_slice, + url::Url, +}; +use tempfile::TempDir; + +pub fn print_now(disp: impl Display) { + print!("{}", disp); + std::io::stdout().flush().unwrap(); +} + +#[must_use] +pub fn check_incoming(base_url: &str, txs: &[([u8; 32], Amount)]) -> bool { + let history: IncomingHistory = ureq::get(&format!("{}history/incoming", base_url)) + .query("delta", &format!("-{}", txs.len())) + .call() + .unwrap() + .into_json() + .unwrap(); + + history.incoming_transactions.len() == txs.len() + && txs.iter().all(|(reserve_pub_key, taler_amount)| { + history.incoming_transactions.iter().any(|h| { + matches!( + h, + IncomingBankTransaction::IncomingReserveTransaction { + reserve_pub, + amount, + .. + } if reserve_pub == &Base32::from(*reserve_pub_key) && amount == taler_amount + ) + }) + }) +} + +pub fn gateway_error(path: &str, error: u16) { + let err = ureq::get(path).call().unwrap_err(); + match err { + ureq::Error::Status(nb, _) => assert_eq!(nb, error), + ureq::Error::Transport(_) => unreachable!(), + } +} + +#[must_use] +pub fn check_gateway_error(base_url: &str) -> bool { + matches!( + ureq::get(&format!("{}history/incoming", base_url)) + .query("delta", "-5") + .call(), + Err(ureq::Error::Status(504, _)) + ) +} + +#[must_use] +pub fn check_gateway_down(base_url: &str) -> bool { + matches!( + ureq::get(&format!("{}history/incoming", base_url)) + .query("delta", "-5") + .call(), + Err(ureq::Error::Status(502, _)) + ) +} + +#[must_use] +pub fn check_gateway_up(base_url: &str) -> bool { + ureq::get(&format!("{}history/incoming", base_url)) + .query("delta", "-5") + .call() + .is_ok() +} + +pub fn transfer(base_url: &str, wtid: &[u8; 32], url: &Url, credit_account: Url, amount: &Amount) { + ureq::post(&format!("{}transfer", base_url)) + .send_json(TransferRequest { + request_uid: Base32::from(rand_slice()), + amount: amount.clone(), + exchange_base_url: url.clone(), + wtid: Base32::from(*wtid), + credit_account, + }) + .unwrap(); +} + +#[must_use] +pub fn check_outgoing(base_url: &str, url: &Url, txs: &[([u8; 32], Amount)]) -> bool { + let history: OutgoingHistory = ureq::get(&format!("{}history/outgoing", base_url)) + .query("delta", &format!("-{}", txs.len())) + .call() + .unwrap() + .into_json() + .unwrap(); + history.outgoing_transactions.len() == txs.len() + && txs.iter().all(|(wtid, amount)| { + history.outgoing_transactions.iter().any(|h| { + h.wtid == Base32::from(*wtid) && &h.exchange_base_url == url && &h.amount == amount + }) + }) +} + +pub struct Dirs { + pub dir: TempDir, + pub wire_dir: PathBuf, + pub wire_dir2: PathBuf, + pub db_dir: PathBuf, + pub conf: PathBuf, +} + +impl Dirs { + pub fn generate() -> Self { + let dir = TempDir::new().unwrap(); + let wire_dir = dir.path().join("wire"); + let wire_dir2 = dir.path().join("wire2"); + let db_dir = dir.path().join("db"); + for dir in [&wire_dir, &wire_dir2, &db_dir] { + std::fs::create_dir_all(dir).unwrap(); + } + let conf = dir.path().join("taler.conf"); + Self { + wire_dir, + wire_dir2, + db_dir, + conf, + dir, + } + } +} + +pub struct ChildGuard(pub Child); + +impl Drop for ChildGuard { + fn drop(&mut self) { + self.0.kill().ok(); + } +} + +pub fn cmd_out(cmd: &str, args: &[&str]) -> String { + let output = Command::new(cmd) + .args(args) + .stdin(Stdio::null()) + .output() + .unwrap(); + if output.stdout.is_empty() { + String::from_utf8(output.stderr).unwrap() + } else { + String::from_utf8(output.stdout).unwrap() + } +} + +pub fn try_cmd_redirect(cmd: &str, args: &[&str], path: &str) -> std::io::Result<ChildGuard> { + let log_file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + + let child = Command::new(cmd) + .args(args) + .stderr(log_file.try_clone()?) + .stdout(log_file) + .stdin(Stdio::null()) + .spawn()?; + Ok(ChildGuard(child)) +} + +pub fn cmd_redirect(cmd: &str, args: &[&str], path: &str) -> ChildGuard { + try_cmd_redirect(cmd, args, path).unwrap() +} + +pub fn cmd_ok(mut child: ChildGuard, name: &str) { + let result = child.0.wait().unwrap(); + if !result.success() { + panic!("cmd {name} failed"); + } +} + +pub fn cmd_redirect_ok(cmd: &str, args: &[&str], path: &str, name: &str) { + cmd_ok(cmd_redirect(cmd, args, path), name) +} + +pub fn retry_opt<T>(mut lambda: impl FnMut() -> Option<T>) -> T { + let start = Instant::now(); + loop { + let result = lambda(); + if result.is_none() && start.elapsed() < Duration::from_secs(20) { + sleep(Duration::from_millis(500)); + } else { + return result.unwrap(); + } + } +} + +pub fn retry(mut lambda: impl FnMut() -> bool) { + retry_opt(|| lambda().then(|| ())) +} + +pub struct CommonCtx { + pub dirs: Dirs, + gateway: ChildGuard, + pub gateway_url: String, + pub taler_conf: TalerConfig, + wire: ChildGuard, + _wire2: Option<ChildGuard>, +} + +impl CommonCtx { + pub fn setup_dirs(config: &str) -> Dirs { + // Setup logs + { + // Create log dir if no exist + std::fs::create_dir_all("log").unwrap(); + // Clear all previous logs file + for file in std::fs::read_dir("log").unwrap() { + std::fs::write(file.unwrap().path(), "").unwrap(); + } + } + // Generate temporary dirs + let dirs = Dirs::generate(); + // Prepare config + { + // Generate taler config from base + let config = PathBuf::from_str("instrumentation/conf") + .unwrap() + .join(config); + let mut config = std::fs::read_to_string(config).unwrap(); + write!( + &mut config, + "\nCONF_PATH = {}\nIPC_PATH={}", + dirs.wire_dir.to_string_lossy(), + dirs.wire_dir.to_string_lossy() + ) + .unwrap(); + std::fs::write(&dirs.conf, config).unwrap(); + } + + // Generate password + let pwd: String = (0..30).map(|_| fastrand::alphanumeric()).collect(); + std::env::set_var("PASSWORD", &pwd); + + dirs + } + + pub fn setup(dirs: Dirs, name: &str, stressed: bool, init_wallet: impl FnOnce(&Dirs)) -> Self { + let taler_conf = TalerConfig::load(Some(&dirs.conf)); + // Setup database + { + // Init databases files + cmd_redirect_ok( + "pg_ctl", + &["init", "-D", dirs.db_dir.to_string_lossy().as_ref()], + "log/postgres.log", + "init_db", + ); + // Generate database config + std::fs::write( + dirs.db_dir.join("postgresql.conf"), + format!( + "port=5454\nunix_socket_directories='{}'\n", + dirs.db_dir.to_string_lossy().as_ref() + ), + ) + .unwrap(); + Self::_start_db(&dirs.db_dir); + + let mut psql = ChildGuard( + Command::new("psql") + .args(["-h", "localhost", "-p", "5454", "postgres"]) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .stdin(Stdio::piped()) + .spawn() + .unwrap(), + ); + psql.0 + .stdin + .as_mut() + .unwrap() + .write_all("CREATE ROLE postgres LOGIN SUPERUSER PASSWORD 'password'".as_bytes()) + .unwrap(); + cmd_ok(psql, "psql setup"); + } + + // Wire + let wire_path = format!("target/release/{}", name); + let mut args = vec!["build", "--bin", name, "--release"]; + if stressed { + args.extend_from_slice(&["--features", "fail"]); + } + cmd_redirect_ok("cargo", &args, "log/cargo.log", "build wire"); + cmd_redirect_ok( + &wire_path, + &["-c", dirs.conf.to_string_lossy().as_ref(), "initdb"], + "log/cmd.log", + "wire initdb", + ); + + init_wallet(&dirs); + + // Start wires + let wire = cmd_redirect( + &wire_path, + &["-c", dirs.conf.to_string_lossy().as_ref()], + "log/wire.log", + ); + let wire2 = stressed.then(|| { + cmd_redirect( + &wire_path, + &["-c", dirs.conf.to_string_lossy().as_ref()], + "log/wire2.log", + ) + }); + + // Gateway + cmd_redirect_ok( + "cargo", + &[ + "build", + "--bin", + "wire-gateway", + "--release", + "--features", + "test", + ], + "log/cargo.log", + "build gateway", + ); + let gateway = cmd_redirect( + "target/release/wire-gateway", + &["-c", dirs.conf.to_string_lossy().as_ref()], + "log/gateway.log", + ); + + let gateway_url = format!("http://localhost:{}/", taler_conf.port()); + + Self { + dirs, + gateway, + gateway_url, + taler_conf, + wire, + _wire2: wire2, + } + } + + /* ----- Process ----- */ + + #[must_use] + pub fn wire_running(&mut self) -> bool { + self.wire.0.try_wait().unwrap().is_none() + } + + #[must_use] + pub fn gateway_running(&mut self) -> bool { + self.gateway.0.try_wait().unwrap().is_none() + } + + /* ----- Database ----- */ + + fn _start_db(path: &Path) { + cmd_redirect_ok( + "pg_ctl", + &["start", "-D", path.to_string_lossy().as_ref()], + "log/postgres.log", + "start db", + ) + } + + fn _stop_db(path: &Path, log: &Path) -> std::io::Result<ChildGuard> { + try_cmd_redirect( + "pg_ctl", + &["stop", "-D", path.to_string_lossy().as_ref()], + log.to_string_lossy().as_ref(), + ) + } + + pub fn resume_db(&self) { + Self::_start_db(&self.dirs.db_dir) + } + + pub fn stop_db(&self) { + cmd_ok( + Self::_stop_db(&self.dirs.db_dir, "log/postgres.log".as_ref()).unwrap(), + "stop db", + ) + } + + /* ----- Wire Gateway -----*/ + + pub fn expect_credits(&self, txs: &[([u8; 32], Amount)]) { + retry(|| check_incoming(&self.gateway_url, txs)) + } + + pub fn expect_debits(&self, base_url: &Url, txs: &[([u8; 32], Amount)]) { + retry(|| check_outgoing(&self.gateway_url, base_url, txs)) + } + + pub fn expect_error(&self) { + retry(|| check_gateway_error(&self.gateway_url)); + } + + pub fn expect_gateway_up(&self) { + retry(|| check_gateway_up(&self.gateway_url)); + } + + pub fn expect_gateway_down(&self) { + retry(|| check_gateway_down(&self.gateway_url)); + } +} + +impl Drop for CommonCtx { + fn drop(&mut self) { + Self::_stop_db(&self.dirs.db_dir, "/dev/null".as_ref()) + .and_then(|mut it| it.0.wait()) + .ok(); + } +} diff --git a/makefile b/makefile @@ -3,40 +3,5 @@ install: cargo install --path eth-wire --bin eth-wire cargo install --path wire-gateway -install_test: install - cargo install --path btc-wire --bin btc-wire-utils - cargo install --path eth-wire --bin eth-wire-utils - cargo install --path instrumentation - -test_gateway: install_test - test/gateway/api.sh - test/gateway/auth.sh - -test_btc: install_test - test/btc/wire.sh - test/btc/lifetime.sh - test/btc/reconnect.sh - test/btc/stress.sh - test/btc/conflict.sh - test/btc/reorg.sh - test/btc/hell.sh - test/btc/analysis.sh - test/btc/bumpfee.sh - test/btc/maxfee.sh - test/btc/config.sh - -test_eth: install_test - test/eth/wire.sh - test/eth/lifetime.sh - test/eth/reconnect.sh - test/eth/stress.sh - test/eth/reorg.sh - test/eth/hell.sh - test/eth/analysis.sh - test/eth/bumpfee.sh - test/eth/maxfee.sh - -test: test_gateway test_eth test_btc - segwit_demo: cargo run --release --bin segwit-demo \ No newline at end of file diff --git a/script/prepare.sh b/script/prepare.sh @@ -22,7 +22,7 @@ echo "Found version $PG_VER" echo "â…¡ - Install bitcoind version 0.22" cd $DIR -curl -L https://bitcoin.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz -o btc.tar.gz +curl -L https://bitcoincore.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz -o btc.tar.gz rm -rfv ~/bitcoin mkdir -pv ~/bitcoin tar xvzf btc.tar.gz diff --git a/test/btc/analysis.sh b/test/btc/analysis.sh @@ -1,63 +0,0 @@ -#!/bin/bash - -## Test btc-wire ability to learn and protect itself from blockchain behavior - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Learn from reorg -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS transfer 0.042 > /dev/null -next_btc # Trigger btc-wire -check_balance 9.95799209 0.04200000 -echo " OK" - -echo -n "Perform fork and check btc-wire hard error:" -gateway_up -btc_fork 5 -check_balance 9.95799209 0.00000000 -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_btc 5 # More block needed to confirm -check_balance 9.95799209 0.04200000 -gateway_up -echo " OK" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS transfer 0.064 > /dev/null -next_btc 5 # More block needed to confirm -check_balance 9.89398418 0.10600000 -echo " OK" - -echo -n "Perform fork and check btc-wire learned from previous attack:" -gateway_up -btc_fork 5 -check_balance 9.89398418 0.10600000 -gateway_up -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/bumpfee.sh b/test/btc/bumpfee.sh @@ -1,112 +0,0 @@ -#!/bin/bash - -## Test btc-wire ability to handle stuck transaction correctly - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc_bump.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 30" - -echo -n "Making wire transfer to exchange:" -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Trigger btc-wire -check_balance 5.79983389 4.20000000 -echo " OK" - -echo "----- Bump fee -----" - -echo -n "Making wire transfer from exchange:" -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.004 > /dev/null -sleep 1 -check_balance 5.79983389 4.19599801 -echo " OK" - -echo -n "Bump relay fee:" -restart_btc -minrelaytxfee=0.0001 -echo " OK" - -echo -n "Check bump:" -sleep 5 -mine_btc -sleep 1 -check_balance 5.80383389 4.19598010 -echo " OK" - -echo "----- Bump fee reorg -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Making wire transfer from exchange:" -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.004 > /dev/null -sleep 1 -check_balance 5.80383389 4.19196020 -echo " OK" - -echo -n "Perform fork and bump relay fee:" -btc_fork 6 -restart_btc -minrelaytxfee=0.0002 -mine_btc -echo " OK" - -echo -n "Check bump:" -sleep 5 -next_btc -sleep 1 -check_balance 5.80783389 4.19194030 -echo " OK" - -echo "----- Bump fee stress -----" - -echo -n "Replace btc-wire with stressed btc-wire" -kill $WIRE_PID -stress_btc_wire -echo " OK" - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.00$n > /dev/null -done -sleep 5 -echo " OK" - -echo -n "Bump relay fee:" -restart_btc -minrelaytxfee=0.0003 -echo " OK" - -echo -n "Check bump:" -for n in `seq 0 4`; do - sleep 2 - mine_btc -done -check_balance 5.84983389 4.14868660 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/config.sh b/test/btc/config.sh @@ -1,27 +0,0 @@ -#!/bin/bash - -## Test btc-wire ability to configure itself from bitcoin configuration - -set -eu - -CONFIG=taler_btc.conf - -function test() { - echo "----- Config $1 -----" - - BTC_CONFIG=$1 - source "${BASH_SOURCE%/*}/../common.sh" - - echo "Load config file" - load_config - echo "Start bitcoin node" - init_btc - echo "Cleanup" - cleanup -} - -for n in `seq 0 5`; do - test "bitcoin_auth$n.conf" -done - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/conflict.sh b/test/btc/conflict.sh @@ -1,110 +0,0 @@ -#!/bin/bash - -## Test btc-wire ability to handle conflicting outgoing transactions - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Conflict send -----" - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS transfer 0.042 > /dev/null -next_btc -check_balance 9.95799209 0.04200000 -echo " OK" - -echo -n "Making wire transfer from exchange:" -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.004 > /dev/null -sleep 1 -check_balance 9.95799209 0.03799801 -echo " OK" - -echo -n "Abandon pending transaction:" -restart_btc -minrelaytxfee=0.0001 -$WIRE_UTILS abandon -check_balance 9.95799209 0.04200000 -echo " OK" - -echo -n "Generate conflict:" -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.005 > /dev/null -sleep 5 -restart_btc -mine_btc -check_balance 9.96299209 0.03698010 -echo " OK" - -echo -n "Resend conflicting transaction:" -sleep 5 # Wait for reconnection -mine_btc -check_delta "outgoing?delta=-100" "seq 4 5" "0.00" -check_balance 9.96699209 0.03297811 -echo " OK" - -echo "----- Reset -----" -echo "Cleanup" -cleanup -source "${BASH_SOURCE%/*}/../common.sh" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Conflict bounce -----" - -echo -n "Bounce:" -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.04 > /dev/null -mine_btc $CONFIRMATION -sleep 1 -check_balance 9.95999859 0.00001000 -echo " OK" - -echo -n "Abandon pending transaction:" -restart_btc -minrelaytxfee=0.0001 -$WIRE_UTILS abandon -check_balance 9.95999859 0.04000000 -echo " OK" - -echo -n "Generate conflict:" -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.05 > /dev/null -mine_btc $CONFIRMATION -sleep 1 -restart_btc -mine_btc -check_balance 9.94997264 0.05001000 -echo " OK" - -echo -n "Resend conflicting transaction:" -sleep 5 # Wait for reconnection -mine_btc -check_balance 9.99996079 0.00002000 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/hell.sh b/test/btc/hell.sh @@ -1,112 +0,0 @@ -#!/bin/bash - -## Test btc-wire correctness when a blockchain reorganization occurs leading to past incoming transaction conflict - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Handle reorg conflicting incoming credit -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Gen incoming transactions:" -$WIRE_UTILS transfer 0.0042 > /dev/null -next_btc # Trigger btc-wire -check_balance 9.99579209 0.00420000 -echo " OK" - -echo -n "Perform fork and check btc-wire hard error:" -gateway_up -btc_fork 5 -check_balance 9.99579209 0.00000000 -gateway_down -echo " OK" - -echo -n "Generate conflict:" -restart_btc -minrelaytxfee=0.0001 -$WIRE_UTILS abandon client -$WIRE_UTILS transfer 0.0054 > /dev/null -mine_btc -check_balance 9.99457382 0.00540000 -echo " OK" - -echo -n "Check btc-wire suspend function:" -sleep 5 # Wait for reconnection -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.0042 > /dev/null -next_btc -sleep 1 -gateway_down -check_balance 9.99035972 0.00960000 # Not bounced -echo " OK" - -# Recover by paying for the customer ? - -echo "----- Reset -----" -echo "Cleanup" -cleanup -source "${BASH_SOURCE%/*}/../common.sh" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Handle reorg conflicting incoming bounce -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Generate bounce:" -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.042 > /dev/null -next_btc -check_balance 9.99998674 0.00001000 -echo " OK" - -echo -n "Perform fork and check btc-wire hard error:" -gateway_up -btc_fork 5 -check_balance 9.95799859 0.00000000 -gateway_down -echo " OK" - -echo -n "Generate conflict:" -restart_btc -minrelaytxfee=0.0001 -$WIRE_UTILS abandon client -$WIRE_UTILS transfer 0.054 > /dev/null -mine_btc -check_balance 9.94597382 0.05400000 -echo " OK" - -echo -n "Check btc-wire suspend function:" -sleep 5 # Wait for reconnection -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.0042 > /dev/null -next_btc -sleep 1 -gateway_down -check_balance 9.94175972 0.05820000 # Not bounced -echo " OK" - - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/lifetime.sh b/test/btc/lifetime.sh @@ -1,51 +0,0 @@ -#!/bin/bash - -## Check btc-wire and wire-gateway correctly stop when a lifetime limit is configured - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc_lifetime.conf - - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo "---- Check lifetime -----" - -echo -n "Check up:" -check_up $WIRE_PID btc-wire -check_up $GATEWAY_PID wire-gateway -echo " OK" - -echo -n "Do some work:" -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.000$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Trigger btc-wire -check_balance 9.99826299 0.00165000 -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n &> /dev/null || break; -done -echo " OK" - -echo -n "Check down:" -check_down $WIRE_PID btc-wire -check_down $GATEWAY_PID wire-gateway -echo " OK" - -echo "All tests passed!" diff --git a/test/btc/maxfee.sh b/test/btc/maxfee.sh @@ -1,64 +0,0 @@ -#!/bin/bash - -## Test btc-wire handle transaction fees exceeding limits - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 30" - -echo -n "Making wire transfer to exchange: " -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Trigger btc-wire -check_balance 5.79983389 4.20000000 -echo "OK" - -echo "----- Too high fees -----" - -echo -n "Set up node: " -restart_btc -maxtxfee=0.0000001 -minrelaytxfee=0.0000001 -echo "OK" - -echo -n "Making wire transfer from exchange: " -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0$n > /dev/null -done -sleep 5 -mine_btc -echo "OK" - -echo -n "Check no transaction have been made: " -check_balance 5.79983389 4.20000000 -echo "OK" - -echo "----- Good fees -----" - -echo -n "Set up node: " -restart_btc -echo "OK" - -echo -n "Check transaction have been made: " -sleep 6 -check_balance 5.79983389 3.77995821 -echo "OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/reconnect.sh b/test/btc/reconnect.sh @@ -1,86 +0,0 @@ -#!/bin/bash - -## Check the capacity of wire-gateway and btc-wire to recover from database and node loss - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -echo "----- With DB -----" -echo "Making wire transfer to exchange:" -$WIRE_UTILS transfer 0.000042 > /dev/null -next_btc -check_balance 9.99995009 0.00004200 -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "echo 42" -echo "OK" - -echo "----- Without DB -----" - -echo "Stop database" -stop_db -echo "Making incomplete wire transfer to exchange" -$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.00042 &> /dev/null -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS transfer 0.00004 > /dev/null -next_btc -check_balance 9.99948077 0.00050200 -echo " OK" -echo "Stop bitcoin node" -stop_node -echo -n "Requesting exchange incoming transaction list:" -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i 2>&1 | grep -q "504" && echo " OK" || echo " Failed" - -echo "----- Reconnect DB -----" - -echo "Start database" -start_db -echo "Resume bitcoin node" -resume_btc -sleep 6 # Wait for connection to be available -echo -n "Making wire transfer from exchange:" -taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.00002 > /dev/null -sleep 1 -mine_btc -check_balance 9.99990892 0.00007001 -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "echo 2" -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_btc # Trigger worker -sleep 2 - -echo -n "Checking recover incoming transactions:" -check_delta "incoming?delta=-100" "echo 42 4" -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "echo 2" -echo " OK" - -echo -n "Balance should not have changed:" -check_balance 9.99990892 0.00007001 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/reorg.sh b/test/btc/reorg.sh @@ -1,114 +0,0 @@ -#!/bin/bash - -## Test btc-wire correctness when a blockchain reorganization occurs - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start second bitcoin node" -init_btc2 -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo "----- Handle reorg incoming transactions -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Gen incoming transactions:" -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.000$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Trigger btc-wire -check_delta "incoming?delta=-100" "$SEQ" "0.000" -check_balance 9.99826299 0.00165000 -echo " OK" - -echo -n "Perform fork and check btc-wire hard error:" -gateway_up -btc_fork 22 -check_balance 9.99826299 0.00000000 -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_btc 6 # More block needed to confirm -check_balance 9.99826299 0.00165000 -gateway_up -echo " OK" - -echo "----- Handle reorg outgoing transactions -----" - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Gen outgoing transactions:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -mine_btc # Mine transactions -check_delta "outgoing?delta=-100" "$SEQ" -check_balance 9.99842799 0.00146311 -echo " OK" - -echo -n "Perform fork and check btc-wire still up:" -gateway_up -btc_fork 22 -check_balance 9.99826299 0.00146311 -gateway_up -echo " OK" - -echo -n "Recover orphaned transactions:" -next_btc 6 # More block needed to confirm -check_balance 9.99842799 0.00146311 -echo " OK" - -echo "----- Handle reorg bounce -----" - -clear_wallet -check_balance "*" 0.00000000 - -echo "Loose second bitcoin node" -btc_deco - -echo -n "Generate bounce:" -for n in `$SEQ`; do - $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.000$n > /dev/null - mine_btc -done -next_btc 6 # More block needed to confirm -sleep 1 -check_balance "*" 0.00011000 -echo " OK" - -echo -n "Perform fork and check btc-wire hard error:" -gateway_up -btc_fork 22 -check_balance "*" 0.00000000 -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_btc 6 # More block needed to confirm -check_balance "*" 0.00011000 -gateway_up -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/stress.sh b/test/btc/stress.sh @@ -1,123 +0,0 @@ -#!/bin/bash - -## Test btc-wire ability to recover from errors in correctness critical paths and prevent concurrent sending - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup stressed -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start stressed btc-wire" -stress_btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 30" - -echo "----- Handle incoming -----" - -echo -n "Making wire transfer to exchange:" -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.000$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Confirm all transactions -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Check balance:" -check_balance 9.99563389 0.00420000 -echo " OK" - -echo "----- Handle outgoing -----" - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 10 # Give time for btc-wire worker to process -next_btc # Mine transactions -echo " OK" - -echo -n "Requesting exchange outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Check balance:" -check_balance 9.99605389 0.00373821 -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_btc # Trigger worker -sleep 10 - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Requesting exchange outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Check balance:" -# Balance should not have changed -check_balance 9.99605389 0.00373821 -echo " OK" - -echo "----- Handle bounce -----" - -echo -n "Clear wire wallet:" -$BTC_CLI -rpcwallet=wire sendtoaddress $CLIENT `$BTC_CLI -rpcwallet=wire getbalance` "" "" true > /dev/null -echo " OK" - -echo -n "Making incomplete wire transfer to exchange:" -for n in `$SEQ`; do - $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.000$n > /dev/null - mine_btc -done -next_btc -sleep 5 -mine_btc -sleep 5 -echo " OK" - -echo -n "Check balance:" -check_balance "*" 0.00021000 -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_btc # Trigger worker -sleep 10 - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Requesting exchange outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Check balance:" -# Balance should not have changed -check_balance "*" 0.00021000 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/btc/wire.sh b/test/btc/wire.sh @@ -1,70 +0,0 @@ -#!/bin/bash - -## Test btc-wire correctly receive and send transactions on the blockchain - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start btc-wire" -btc_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 99" - -echo "----- Receive -----" - -echo -n "Making wire transfer to exchange:" -for n in `$SEQ`; do - $WIRE_UTILS transfer 0.000$n > /dev/null - mine_btc # Mine transactions -done -next_btc # Trigger btc-wire -check_balance 9.95023810 0.04905000 -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo "----- Send -----" - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 15 -mine_btc # Mine transactions -check_balance 9.95514310 "" -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo "----- Bounce -----" - -clear_wallet - -echo -n "Bounce:" -for n in `seq 10 40`; do - $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.000$n > /dev/null - mine_btc -done -next_btc -sleep 3 -check_balance "*" 0.00031000 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/common.sh b/test/common.sh @@ -1,405 +0,0 @@ -#!/bin/bash - -## Test utils - -set -eu - -# Cleanup to run whenever we exit -function cleanup() { - pg_ctl stop -D $DB_DIR -w &> /dev/null - for n in `jobs -p`; do - kill $n &> /dev/null || true - done - rm -rf $DIR &> /dev/null || true - wait -} - -# Install cleanup handler (except for kill -9) -trap cleanup EXIT - -# Init temporary dirs -DIR=$(mktemp -d) -WIRE_DIR=$DIR/wire -WIRE_DIR2=$DIR/wire2 -DB_DIR=$DIR/db -CONF=$DIR/taler.conf -for dir in $WIRE_DIR $WIRE_DIR2 $DB_DIR log; do - mkdir -p "$dir" -done - -# Clear logs -for log in log/*; do - echo -n "" > $log -done - -# Generate random password -export PASSWORD=`cat /proc/sys/kernel/random/uuid` - -# Setup command helpers -BTC_CLI="bitcoin-cli -datadir=$WIRE_DIR" -BTC_CLI2="bitcoin-cli -datadir=$WIRE_DIR2" -ETH_CLI="geth -datadir=$WIRE_DIR" -ETH_CLI2="geth -datadir=$WIRE_DIR2" - - -# ----- Common ----- # - -# Load test.conf as bash variables -function load_config() { - cp ${BASH_SOURCE%/*}/conf/$CONFIG $CONF - echo -e "\nCONF_PATH = ${WIRE_DIR}" >> $CONF - echo -e "IPC_PATH = ${WIRE_DIR}" >> $CONF - source <(grep = $CONF | sed 's/ *= */=/' | sed 's/=\(.*\)/="\1"/g1') - BANK_ENDPOINT=http://127.0.0.1:${PORT:-8080}/ - if [[ "$CURRENCY" =~ "BTC" ]]; then - WIRE_CLI="btc-wire -c $CONF" - WIRE_UTILS="btc-wire-utils -c $CONF" - WIRE_UTILS2="btc-wire-utils -c $CONF -d $WIRE_DIR2" - else - WIRE_CLI="eth-wire -c $CONF" - WIRE_UTILS="eth-wire-utils -c $CONF" - WIRE_UTILS2="eth-wire-utils -c $CONF -d $WIRE_DIR2" - fi -} - -# Check process is running -function check_up() { - if [ `ps -p $1 | grep -c $1` == 0 ]; then - echo "${2:-process} with pid $1 should be up" - ps -p $1 - exit 1 - fi -} - -# Check process is not running -function check_down() { - if [ `ps -p $1 | grep -c $1` != 0 ]; then - echo "${2:-process} with pid $1 should be down" - ps -p $1 - exit 1 - fi -} - -function stop_node() { - kill $NODE_PID - wait $NODE_PID -} - -# ----- Database ----- # - -# Create new postgresql cluster and init database -function setup_db() { - pg_ctl init -D $DB_DIR &>> log/postgres.log - echo "port=5454" >> $DB_DIR/postgresql.conf - start_db - echo "CREATE ROLE postgres LOGIN SUPERUSER PASSWORD 'password'" | psql -h localhost -p 5454 postgres > /dev/null - $WIRE_CLI initdb > /dev/null -} - -# Erase database -function reset_db() { - sleep 5 # Wait for loop to stop - $WIRE_UTILS resetdb -} - -# Stop database -function stop_db() { - pg_ctl stop -D $DB_DIR >> log/postgres.log -} - -# Start database -function start_db() { - pg_ctl start -D $DB_DIR -o "-c unix_socket_directories=$DIR" >> log/postgres.log -} - -# ----- Bitcoin node ----- # - -# Start a bitcoind regtest node, generate money, wallet and addresses -function init_btc() { - cp ${BASH_SOURCE%/*}/conf/${BTC_CONFIG:-bitcoin.conf} $WIRE_DIR/bitcoin.conf - bitcoind -datadir=$WIRE_DIR $* &>> log/node.log & - NODE_PID="$!" - # Wait for RPC server to be online - $BTC_CLI -rpcwait getnetworkinfo > /dev/null - # Setup db - setup_db - # Create wire wallet - $WIRE_CLI initwallet > /dev/null - # Create other wallets - for wallet in client reserve; do - $BTC_CLI createwallet $wallet > /dev/null - done - # Generate addresses - RESERVE=`$BTC_CLI -rpcwallet=reserve getnewaddress` - CLIENT=`$BTC_CLI -rpcwallet=client getnewaddress` - WIRE=`$BTC_CLI -rpcwallet=wire getnewaddress` - # Generate money - mine_btc 101 - $BTC_CLI -rpcwallet=reserve sendtoaddress $CLIENT 10 > /dev/null - mine_btc -} - -# Start a second bitcoind regtest node connected to the first one -function init_btc2() { - cp ${BASH_SOURCE%/*}/conf/bitcoin2.conf $WIRE_DIR2/bitcoin.conf - bitcoind -datadir=$WIRE_DIR2 $* &>> log/node2.log & - $BTC_CLI2 -rpcwait getnetworkinfo > /dev/null - $BTC_CLI addnode 127.0.0.1:8346 onetry -} - -# Disconnect the two nodes -function btc_deco() { - $BTC_CLI disconnectnode 127.0.0.1:8346 -} - -# Create a fork on the second node and reconnect the two node -function btc_fork() { - $BTC_CLI2 generatetoaddress $1 $RESERVE > /dev/null - $BTC_CLI addnode 127.0.0.1:8346 onetry - sleep 1 -} - -# Restart a bitcoind regest server in a previously created temporary directory and load wallets -function resume_btc() { - # Restart node - bitcoind -datadir=$WIRE_DIR $* &>> log/node.log & - NODE_PID="$!" - # Load wallets - for wallet in wire client reserve; do - $BTC_CLI -rpcwait loadwallet $wallet > /dev/null - done - # Connect second node - $BTC_CLI addnode 127.0.0.1:8346 onetry -} - -function restart_btc() { - stop_node - resume_btc $* -} - -# Mine bitcoin blocks -function mine_btc() { - $BTC_CLI generatetoaddress "${1:-1}" $RESERVE > /dev/null -} - -# Mine previous transactions -function next_btc() { - # Mine enough block to confirm previous transactions - mine_btc ${1:-$CONFIRMATION} - # Wait for btc-wire to catch up - sleep 0.2 - # Mine one more block to trigger btc-wire - mine_btc -} - -# Remove money from wallet wire -function clear_wallet() { - $BTC_CLI -rpcwallet=wire sendtoaddress $CLIENT `$BTC_CLI -rpcwallet=wire getbalance` "" "" true > /dev/null - mine_btc -} - -# Check client and wire balance -function check_balance() { - local CLIENT_BALANCE=`$BTC_CLI -rpcwallet=client getbalance` - local WIRE_BALANCE=`$BTC_CLI -rpcwallet=wire getbalance` - local CLIENT="${1:-*}" - if [ "$1" == "*" ]; then - local CLIENT="$CLIENT_BALANCE" - fi - if [ "$CLIENT_BALANCE" != "$CLIENT" ] || [ "$WIRE_BALANCE" != "${2:-$WIRE_BALANCE}" ]; then - echo "expected: client $CLIENT wire ${2:-$WIRE_BALANCE} got: client $CLIENT_BALANCE wire $WIRE_BALANCE" - exit 1 - fi -} - -# ----- btc-wire ----- # - -# Start btc-wire -function btc_wire() { - cargo build --bin btc-wire --release &>> log/cargo.log - target/release/btc-wire -c $CONF &>> log/wire.log & - WIRE_PID="$!" -} - -# Start multiple btc-wire with random failures in parallel -function stress_btc_wire() { - cargo build --bin btc-wire --release --features fail &>> log/cargo.log - target/release/btc-wire -c $CONF &>> log/wire.log & - target/release/btc-wire -c $CONF &>> log/wire1.log & -} - -# ----- Ethereum node ----- # - -# Start a geth dev node, generate money, wallet and addresses -function init_eth() { - # Create wallets - for pswd in "reserve" "client"; do - $ETH_CLI account new --password <(echo $PASSWORD) &> /dev/null - done - # Retrieve addresses - local ADDR=`$ETH_CLI account list 2> /dev/null | grep -oP '(?<={).*?(?=})'` - RESERVE=`sed -n '1p' <(echo "$ADDR")` - CLIENT=`sed -n '2p' <(echo "$ADDR")` - # Generate genesis - echo "{ - \"config\": { - \"chainId\": 42, - \"homesteadBlock\": 0, - \"eip150Block\": 0, - \"eip155Block\": 0, - \"eip158Block\": 0, - \"byzantiumBlock\": 0, - \"constantinopleBlock\": 0, - \"petersburgBlock\": 0, - \"istanbulBlock\": 0, - \"berlinBlock\": 0, - \"londonBlock:\": 0, - \"ethash\": {} - }, - \"difficulty\": \"1\", - \"gasLimit\": \"0\", - \"baseFeePerGas\": null, - \"alloc\": { - \"$CLIENT\": { \"balance\": \"10000000000000000000\" } - } -}" > $DIR/genesis.json - # Initialize blockchain - $ETH_CLI init $DIR/genesis.json &>> log/node.log - # Start node - start_eth $* - # Setup db - setup_db - # Create wire address - WIRE=`$WIRE_CLI initwallet | grep -oP '(?<=is ).*'` - echo -e "PAYTO = payto://ethereum/$WIRE" >> $CONF -} - -# Initialize a second geth dev node -function init_eth2() { - # Initialize blockchain - $ETH_CLI2 init $DIR/genesis.json &>> log/node2.log - $ETH_CLI2 account new --password <(echo $PASSWORD) &> /dev/null -} - -# We use the import/export chain functionality to simulate a connected node peer -# because local network peer are not reliable -# importChain RPC crash so we have to use the cli for now - -# Disconnect the two nodes -function eth_deco() { - $WIRE_UTILS export $DIR/chain &>> log/node.log - $ETH_CLI2 import $DIR/chain &>> log/node2.log -} - -# Create a fork on the second node and reconnect the two node -function eth_fork() { - $ETH_CLI2 --port 30305 --miner.gasprice 0 &>> log/node2.log & - NODE2_PID="$!" - sleep 1 - $WIRE_UTILS2 mine $RESERVE ${1:-} - $WIRE_UTILS2 export $DIR/chain &>> log/node2.log - kill $NODE2_PID - stop_node - $ETH_CLI import $DIR/chain &>> log/node.log - start_eth --miner.gasprice 0 - sleep 5 # Wait for reconnect -} - -# Restart an initialized geth dev node -function start_eth() { - # Start node - $ETH_CLI $* &>> log/node.log & - NODE_PID="$!" - sleep 2 -} - -function restart_eth() { - stop_node - start_eth $* -} - -# Check client and wire balance -function check_balance_eth() { - $WIRE_UTILS check-balance $CLIENT $1 $WIRE $2 -} - - -# ----- eth-wire ----- # - -# Start eth-wire -function eth_wire() { - cargo build --bin eth-wire --release &>> log/cargo.log - target/release/eth-wire -c $CONF &>> log/wire.log & - WIRE_PID="$!" -} - -# Start multiple eth-wire with random failures in parallel -function stress_eth_wire() { - cargo build --bin eth-wire --release --features fail &>> log/cargo.log - target/release/eth-wire -c $CONF &>> log/wire.log & - target/release/eth-wire -c $CONF &>> log/wire1.log & -} - -# Mine ethereum blocks -function mine_eth() { - $WIRE_UTILS mine $RESERVE ${1:-} -} - -# Mine previous transactions -function next_eth() { - # Mine enough block to confirm previous transactions - mine_eth ${1:-$CONFIRMATION} - # Wait for eth-wire to catch up - sleep 0.2 - # Mine one more block to trigger eth-wire - mine_eth -} - -# ----- Gateway ------ # - -# Start wire-gateway in test mode -function gateway() { - cargo build --bin wire-gateway --release --features test &> /dev/null - target/release/wire-gateway -c $CONF &>> log/gateway.log & - GATEWAY_PID="$!" - for n in `seq 1 50`; do - echo -n "." - sleep 0.2 - curl -s $BANK_ENDPOINT -o /dev/null && break - done -} - -# Check wire-gateway is healthy -function gateway_up() { - if [ `curl -w %{http_code} -s ${BANK_ENDPOINT}history/outgoing -o /dev/null` -ne 400 ]; then - echo "gateway should be up" - exit 1 - fi -} - -# Check wire-gateway is down (backend is blocked) -function gateway_down() { - if [ `curl -w %{http_code} -s ${BANK_ENDPOINT}history/outgoing -o /dev/null` -ne 502 ]; then - echo "gateway should be down" - exit 1 - fi -} - -# Check history endpoint request return a specific amount of transactions of specific amounts -# usage: check_delta endpoint nb_txs amount_sequence -function check_delta() { - ALL=`curl -s ${BANK_ENDPOINT}history/$1` - PRE=${3:-0.0000} - for n in `$2`; do - if ! `echo $ALL | grep $CURRENCY:$PRE$n > /dev/null`; then - echo -n " missing tx with amount: $CURRENCY:$PRE$n" - return 1 - fi - done - NB=`echo $ALL | grep -o $CURRENCY:$PRE | wc -l` - EXPECTED=`$2 | wc -w` - if [ "$EXPECTED" != "$NB" ]; then - echo -n " expected: $EXPECTED txs found $NB" - return 1 - fi -} diff --git a/test/conf/bitcoin.conf b/test/conf/bitcoin.conf @@ -1,8 +0,0 @@ -regtest=1 -txindex=1 -maxtxfee=0.01 -fallbackfee=0.00000001 - -[regtest] -port=8345 -rpcport=18345 -\ No newline at end of file diff --git a/test/conf/bitcoin2.conf b/test/conf/bitcoin2.conf @@ -1,7 +0,0 @@ -regtest=1 -txindex=1 -fallbackfee=0.00000001 - -[regtest] -port=8346 -rpcport=18346 -\ No newline at end of file diff --git a/test/conf/bitcoin_auth1.conf b/test/conf/bitcoin_auth1.conf @@ -1,12 +0,0 @@ -regtest=1 -txindex=1 -maxtxfee=0.1 -fallbackfee=0.00000001 -rpcuser=bob -rpcpassword=password - -[regtest] -port=8346 -rpcport=18346 -rpcuser=alice -rpcpassword=password -\ No newline at end of file diff --git a/test/conf/taler_btc_lifetime.conf b/test/conf/taler_btc_lifetime.conf @@ -1,14 +0,0 @@ -[taler] -CURRENCY = DEVBTC - -[exchange] -BASE_URL = http://test.com - -[depolymerizer-bitcoin] -DB_URL = postgres://localhost:5454/postgres?user=postgres&password=password -PORT = 8060 -PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj -CONFIRMATION = 3 -HTTP_LIFETIME = 10 -WIRE_LIFETIME = 10 -AUTH_METHOD = none -\ No newline at end of file diff --git a/test/eth/analysis.sh b/test/eth/analysis.sh @@ -1,63 +0,0 @@ -#!/bin/bash - -## Test eth-wire ability to learn and protect itself from blockchain behavior - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start second ethereum node" -init_eth2 --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Learn from reorg -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.00 42 -next_eth # Trigger eth-wire -check_balance_eth 999580000 420000 -echo " OK" - -echo -n "Perform fork and check eth-wire hard error:" -gateway_up -eth_fork 6 -check_balance_eth 1000000000 0 -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_eth 6 # More block needed to confirm -check_balance_eth 999580000 420000 -gateway_up -echo " OK" - -echo "Loose second bitcoin node" -eth_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.00 42 -next_eth # Trigger eth-wire -check_balance_eth 999160000 840000 -echo " OK" - -echo -n "Perform fork and check eth-wire learned from previous attack:" -gateway_up -eth_fork 10 -check_balance_eth 999580000 420000 -gateway_up -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/bumpfee.sh b/test/eth/bumpfee.sh @@ -1,110 +0,0 @@ -#!/bin/bash - -## Test eth-wire ability to handle stuck transaction correctly - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth_bump.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start second ethereum node" -init_eth2 --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo -n "Making wire transfer to exchange: " -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` -sleep 1 -mine_eth # Trigger eth-wire -check_balance_eth 999835000 165000 -echo "OK" - -echo "----- Bump fee -----" - -echo -n "Making wire transfer from exchange: " -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -echo "OK" - -echo -n "Bump miner fee:" -restart_eth --miner.gasprice 1000 -sleep 5 -echo "OK" - -echo -n "Check bump: " -sleep 1 -mine_eth # Mine transactions -check_balance_eth 999851500 148499 -echo "OK" - -echo "----- Bump fee reorg -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Making wire transfer from exchange: " -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -echo "OK" - -echo -n "Perform fork and bump miner fee:" -eth_fork 10 -restart_eth --miner.gasprice 2000 -sleep 5 -echo "OK" - -echo -n "Check bump: " -sleep 1 -mine_eth # Mine transactions -check_balance_eth 999868000 131999 -echo "OK" - -echo "----- Bump fee stress -----" - -echo -n "Replace btc-wire with stressed btc-wire" -kill $WIRE_PID -stress_eth_wire -echo " OK" - -echo -n "Making wire transfer from exchange: " -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -echo "OK" - -echo -n "Bump miner fee:" -restart_eth --miner.gasprice 3000 -sleep 5 -echo "OK" - -echo -n "Check bump: " -sleep 1 -mine_eth # Mine transactions -check_balance_eth 999884500 115499 -echo "OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/hell.sh b/test/eth/hell.sh @@ -1,107 +0,0 @@ -#!/bin/bash - -## Test eth-wire correctness when a blockchain reorganization occurs leading to past incoming transaction conflict - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start second ethereum node" -init_eth2 --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Handle reorg conflicting incoming credit -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.00 42 -next_eth # Trigger eth-wire -check_balance_eth 999580000 420000 -echo " OK" - -echo -n "Perform fork and check eth-wire hard error:" -gateway_up -eth_fork 10 -check_balance_eth 1000000000 0 -gateway_down -echo " OK" - -echo -n "Generate conflict:" -$WIRE_UTILS abandon $CLIENT -next_eth 5 -check_balance_eth 999999999 0 -echo " OK" - -echo -n "Check eth-wire suspend function:" -gateway_down -$WIRE_UTILS send $CLIENT $WIRE 0.000 42 -next_eth 6 -sleep 1 -check_balance_eth 999957999 42000 # Not bounced -echo " OK" - -# Recover by paying for the customer ? - -echo "----- Reset -----" -echo "Cleanup" -cleanup -source "${BASH_SOURCE%/*}/../common.sh" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start second ethereum node" -init_eth2 --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -echo "----- Handle reorg conflicting incoming bounce -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Bounce:" -$WIRE_UTILS send $CLIENT $WIRE 0.00 42 -next_eth 6 -sleep 1 -check_balance_eth 999999000 1000 -echo " OK" - -echo -n "Perform fork and check eth-wire hard error:" -gateway_up -eth_fork 10 -check_balance_eth 1000000000 0 -gateway_down -echo " OK" - -echo -n "Generate conflict:" -$WIRE_UTILS abandon $CLIENT -next_eth 5 -check_balance_eth 999999999 0 -echo " OK" - -echo -n "Check eth-wire suspend function:" -$WIRE_UTILS send $CLIENT $WIRE 0.000 42 -next_eth 6 -sleep 1 -gateway_down -check_balance_eth 999957999 42000 # Not bounced -echo " OK" - - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/lifetime.sh b/test/eth/lifetime.sh @@ -1,49 +0,0 @@ -#!/bin/bash - -## Check eth-wire and wire-gateway correctly stop when a lifetime limit is configured - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth_lifetime.conf - - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo "---- Check lifetime -----" - -echo -n "Check up:" -check_up $WIRE_PID eth-wire -check_up $GATEWAY_PID wire-gateway -echo " OK" - -echo -n "Do some work:" -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` > /dev/null -next_eth # Trigger eth-wire -check_balance_eth 999835000 165000 -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n &> /dev/null || break; -done -sleep 1 -echo " OK" - -echo -n "Check down:" -check_down $WIRE_PID eth-wire -check_down $GATEWAY_PID wire-gateway -echo " OK" - -echo "All tests passed!" diff --git a/test/eth/maxfee.sh b/test/eth/maxfee.sh @@ -1,65 +0,0 @@ -#!/bin/bash - -## Test eth-wire handle transaction fees exceeding limits - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo -n "Making wire transfer to exchange: " -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` -next_eth # Trigger eth-wire -sleep 1 -check_delta "incoming?delta=-100" "$SEQ" "0.000" -check_balance_eth 999835000 165000 -echo "OK" - - -echo "----- Too high fees -----" - -echo -n "Set up node: " -restart_eth --rpc.txfeecap 0.00001 -echo "OK" - -echo -n "Making wire transfer from exchange: " -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -mine_eth # Mine transactions -echo "OK" - -echo -n "Check no transaction have been made: " -check_balance_eth 999835000 165000 -echo "OK" - -echo "----- Good fees -----" - -echo -n "Set up node: " -restart_eth --miner.gasprice 0 -echo "OK" - -echo -n "Check transaction have been made: " -sleep 5 -mine_eth # Mine transactions -check_balance_eth 999851500 148500 -echo "OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/reconnect.sh b/test/eth/reconnect.sh @@ -1,90 +0,0 @@ -#!/bin/bash - -## Check the capacity of wire-gateway and eth-wire to recover from database and node loss - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -echo "----- With DB -----" - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.0000 42 -next_eth # Trigger eth-wire -check_balance_eth 999995800 4200 -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "echo 42" -echo " OK" - -echo "----- Without DB -----" - -echo "Stop database" -stop_db -echo "Making incomplete wire transfer to exchange" -$WIRE_UTILS send $CLIENT $WIRE 0.0000 42 -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.0000 4 -next_eth -check_balance_eth 999987600 12400 -echo "OK" -echo "Stop ethereum node" -stop_node -echo -n "Requesting exchange incoming transaction list:" -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i 2>&1 | grep -q "504" && echo " OK" || echo " Failed" - -echo "----- Reconnect DB -----" - -echo "Start database" -start_db -echo "Resume ethereum node" -start_eth --miner.gasprice 0 -sleep 6 # Wait for connection to be available -echo -n "Making wire transfer from exchange:" - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.00002 > /dev/null -sleep 1 -mine_eth # Mine transactions -check_balance_eth 999992800 7200 -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "echo 2" -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_eth 1 # Trigger worker -sleep 2 - -echo -n "Checking recover incoming transactions:" -check_delta "incoming?delta=-100" "echo 42 4" -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "echo 2" -echo " OK" - -echo -n "Balance should not have changed:" -check_balance_eth 999992800 7200 -echo " OK" - - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/reorg.sh b/test/eth/reorg.sh @@ -1,106 +0,0 @@ -#!/bin/bash - -## Test eth-wire correctness when a blockchain reorganization occurs - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start second ethereum node" -init_eth2 --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 20" - -echo "----- Handle reorg incoming transactions -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` -next_eth # Trigger eth-wire -check_delta "incoming?delta=-100" "$SEQ" "0.000" -check_balance_eth 999835000 165000 -echo " OK" - -echo -n "Perform fork and check eth-wire hard error:" -gateway_up -eth_fork 10 -check_balance_eth 1000000000 0 -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_eth 6 # More block needed to confirm -check_balance_eth 999835000 165000 -gateway_up -echo " OK" - -echo "----- Handle reorg outgoing transactions -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -mine_eth # Mine transactions -check_delta "outgoing?delta=-100" "$SEQ" -check_balance_eth 999851500 148500 -echo " OK" - -echo -n "Perform fork and check eth-wire still up:" -gateway_up -eth_fork 10 -check_balance_eth 999835000 165000 -gateway_up -echo " OK" - -echo -n "Recover orphaned transactions:" -next_eth 6 # More block needed to confirm -check_balance_eth 999851500 148500 -echo " OK" - -echo "----- Handle reorg bounce -----" - -echo "Loose second ethereum node" -eth_deco - -echo -n "Bounce:" -$WIRE_UTILS send $CLIENT $WIRE 0.000 `$SEQ` -sleep 1 -next_eth 6 -check_balance_eth 999840500 159500 -echo " OK" - -echo -n "Perform fork and check eth-wire hard error:" -gateway_up -eth_fork 10 -check_balance_eth 999851500 148500 -sleep 5 # Wait for reconnect -gateway_down -echo " OK" - -echo -n "Recover orphaned transactions:" -next_eth 6 # More block needed to confirm -check_balance_eth 999840500 159500 -gateway_up -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/stress.sh b/test/eth/stress.sh @@ -1,109 +0,0 @@ -#!/bin/bash - -## Test eth-wire ability to recover from errors in correctness critical paths and prevent concurrent sending - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start stressed eth-wire" -stress_eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 30" - -echo "----- Handle incoming -----" - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` -next_eth # Trigger eth-wire -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Check balance:" -check_balance_eth 999580000 420000 -echo " OK" - -echo "----- Handle outgoing -----" - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -next_eth # Mine transactions -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Check balance:" -check_balance_eth 999622000 378000 -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_eth 1 # Trigger worker -sleep 10 - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Requesting exchange outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Balance have not changed:" -check_balance_eth 999622000 378000 -echo " OK" - -echo "----- Handle bounce -----" - -echo -n "Bounce:" -$WIRE_UTILS send $CLIENT $WIRE 0.000 `$SEQ` -sleep 1 -next_eth -echo " OK" - -echo -n "Check balance:" -check_balance_eth 999601000 399000 -echo " OK" - -echo "----- Recover DB -----" - -echo "Reset database" -reset_db -mine_eth 1 # Trigger worker -sleep 10 - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo -n "Requesting exchange outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo -n "Balance have not changed:" -check_balance_eth 999601000 399000 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/eth/test.sh b/test/eth/test.sh @@ -1,16 +0,0 @@ -#!/bin/bash - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "" - -echo "----- Test -----" -cargo run --bin eth-test --release -- $CONF -\ No newline at end of file diff --git a/test/eth/wire.sh b/test/eth/wire.sh @@ -1,62 +0,0 @@ -#!/bin/bash - -## Test eth-wire correctly receive and send transactions on the blockchain - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_eth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start ethereum node" -init_eth --miner.gasprice 0 -echo "Start eth-wire" -eth_wire -echo "Start gateway" -gateway -echo "" - -SEQ="seq 10 99" - -echo "----- Receive -----" - -echo -n "Making wire transfer to exchange:" -$WIRE_UTILS credit $CLIENT $WIRE 0.000 `$SEQ` -next_eth # Trigger eth-wire -check_balance_eth 995095000 4905000 -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -check_delta "incoming?delta=-100" "$SEQ" "0.000" -echo " OK" - -echo "----- Send -----" - -echo -n "Making wire transfer from exchange:" -for n in `$SEQ`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://ethereum/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -sleep 1 -mine_eth # Mine transactions -check_balance_eth 995585500 4414500 -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -check_delta "outgoing?delta=-100" "$SEQ" -echo " OK" - -echo "----- Bounce -----" - -echo -n "Bounce:" -$WIRE_UTILS send $CLIENT $WIRE 0.000 `seq 10 40` -sleep 1 -next_eth -check_balance_eth 995554500 4445500 -echo " OK" - -echo "All tests passed!" -\ No newline at end of file diff --git a/test/gateway/api.sh b/test/gateway/api.sh @@ -1,133 +0,0 @@ -#!/bin/bash - -## Test wire-gateway conformance to documentation and its security - -set -eu - -# Create temp file -TEMP_FILE=$(mktemp) - -# Cleanup to run whenever we exit -function cleanup() { - rm -f $TEMP_FILE - wait -} - -# Install cleanup handler (except for kill -9) -trap cleanup EXIT - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start gateway" -gateway -echo "" - -echo "----- Gateway API -----" - -echo -n "Making wire transfer to exchange:" -for n in `seq 1 9`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -D payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done -echo " OK" - -echo -n "Requesting exchange incoming transaction list:" -ALL=`taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i` -for n in `seq 1 9`; do - echo $ALL | grep $CURRENCY:0.0000$n > /dev/null -done -echo " OK" - -echo -n "Making wire transfer from exchange:" -for n in `seq 1 9`; do - taler-exchange-wire-gateway-client \ - -b $BANK_ENDPOINT \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -done - -echo " OK" - -echo -n "Requesting exchange's outgoing transaction list:" -ALL=`taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -o` -for n in `seq 1 9`; do - echo $ALL | grep $CURRENCY:0.0000$n > /dev/null -done -echo " OK" - -echo "----- Endpoint & Method -----" - -echo -n "Unknown endpoint:" -test `curl -w %{http_code} -s -o /dev/null ${BANK_ENDPOINT}test` -eq 404 && echo " OK" || echo " Failed" - -echo -n "Method not allowed:" -test `curl -w %{http_code} -s -o /dev/null ${BANK_ENDPOINT}transfer` -eq 405 && echo " OK" || echo " Failed" - -echo "----- Request format -----" - -echo -n "Bad payto url:" -for bad_payto in http://bitcoin/$CLIENT payto://btc/$CLIENT payto://bitcoin/$CLIENT?id=admin payto://bitcoin/$CLIENT#admin; do - taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C $bad_payto -a $CURRENCY:0.00042 2>&1 | grep -q "(400/24)" && echo -n " OK" || echo " Failed" -done -echo "" - -echo -n "Bad bitcoin address:" -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/42$CLIENT -a $CURRENCY:0.00042 2>&1 | grep -q "(400/24)" && echo " OK" || echo " Failed" - -echo -n "Bad transaction amount:" -taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -C payto://bitcoin/$CLIENT -a ATC:0.00042 2>&1 | grep -q "(400/26)" && echo " OK" || echo " Failed" - -echo -n "Bad history delta:" -for bad_delta in incoming outgoing incoming?delta=0 outgoing?delta=0; do - test `curl -w %{http_code} -s -o /dev/null "${BANK_ENDPOINT}history/$bad_delta"` -eq 400 && echo -n " OK" || echo -n " Failed" -done -echo "" - -echo "----- History delta -----" - -for endpoint in incoming outgoing; do - echo -n "History $endpoint:" - check_delta ${endpoint}?delta=-9 "seq 1 9" && echo -n " OK" || echo -n " Failed" - check_delta ${endpoint}?delta=9 "seq 1 9" && echo -n " OK" || echo -n " Failed" - check_delta ${endpoint}?delta=-4 "seq 6 9" && echo -n " OK" || echo -n " Failed" - check_delta ${endpoint}?delta=4 "seq 1 4" && echo -n " OK" || echo -n " Failed" - check_delta "${endpoint}?delta=-3&start=5" "seq 2 4" && echo -n " OK" || echo -n " Failed" - check_delta "${endpoint}?delta=3&start=4" "seq 5 7" && echo -n " OK" || echo -n " Failed" - echo "" -done - -echo "----- Transfer idempotence -----" -DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"$CURRENCY:0.000034\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$CLIENT\"}" -echo -n "Same:" -test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo -n " OK" || echo -n " Failed" -test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 200 && echo " OK" || echo " Failed" -echo -n "Collision:" -DATA="{\"request_uid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"amount\":\"$CURRENCY:0.000042\",\"exchange_base_url\":\"$BASE_URL\",\"wtid\":\"0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00\",\"credit_account\":\"payto://bitcoin/$CLIENT\"}" -test `curl -w %{http_code} -s -o /dev/null -H "Content-Type: application/json" -d $DATA ${BANK_ENDPOINT}transfer` -eq 409 && echo " OK" || echo " Failed" - -echo "----- Security -----" - -# Generate big random file -printf 'HelloWorld%s' {1..1000} > $TEMP_FILE - -echo -n "Handle huge body:" -test `curl -w %{http_code} -X POST -s -o /dev/null -d @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" - -echo -n "Handle body length liar:" -test `curl -w %{http_code} -X POST -H"Content-Length:1024" -s -o /dev/null -d @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" - -# Generate compression bomb -printf 'HelloWorld%s' {1..1000} | pigz -z9 > $TEMP_FILE - -echo -n "Handle compression bomb:" -test `curl -w %{http_code} -X POST -H"Content-Encoding:deflate" -s -o /dev/null --data-binary @$TEMP_FILE ${BANK_ENDPOINT}transfer` -eq 400 && echo " OK" || echo " Failed" - -echo "All tests passed!" diff --git a/test/gateway/auth.sh b/test/gateway/auth.sh @@ -1,31 +0,0 @@ -#!/bin/bash - -## Test wire-gateway authentication - -set -eu - -source "${BASH_SOURCE%/*}/../common.sh" -CONFIG=taler_btc_auth.conf - -echo "----- Setup -----" -echo "Load config file" -load_config -echo "Start bitcoin node" -init_btc -echo "Start gateway" -gateway -echo "" - -echo "----- Authentication -----" - -echo -n "Check 401:" -test `curl -w %{http_code} -s -o /dev/null ${BANK_ENDPOINT}history/outgoing` -eq 401 && echo " OK" || echo " Failed" - -echo -n "Check auth:" -taler-exchange-wire-gateway-client \ - --config $CONF -s exchange-accountcredentials-admin \ - -C payto://bitcoin/$CLIENT \ - -a $CURRENCY:0.0000$n > /dev/null -echo " OK" - -echo "All tests passed!" diff --git a/wire-gateway/src/error.rs b/wire-gateway/src/error.rs @@ -1,128 +1,128 @@ -/* - 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 hyper::{header, Body, Response, StatusCode}; -use common::{api_common::ErrorDetail, error_codes::ErrorCode}; - -/// Generic http error -#[derive(Debug)] -pub struct ServerError { - pub status: StatusCode, - content: Content, - pub msg: String, -} - -#[derive(Debug)] -enum Content { - None, - Json(Vec<u8>), -} - -impl ServerError { - fn new(status: StatusCode, content: Content, msg: String) -> Self { - Self { - status, - content, - msg, - } - } - - pub fn response(self) -> (Response<Body>, String) { - let builder = Response::builder().status(self.status); - let result = match self.content { - Content::None => builder.body(Body::empty()), - Content::Json(it) => builder - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(it)), - }; - (result.unwrap(), self.msg) - } - - pub fn unexpected<E: std::error::Error>(e: E) -> Self { - Self::new( - StatusCode::INTERNAL_SERVER_ERROR, - Content::None, - format!("unexpected: {}", e), - ) - } - - fn detail(status: StatusCode, code: ErrorCode, msg: String) -> Self { - let detail = ErrorDetail { - code: code as i64, - hint: None, - detail: None, - parameter: None, - path: None, - offset: None, - index: None, - object: None, - currency: None, - type_expected: None, - type_actual: None, - }; - match serde_json::to_vec(&detail) { - Ok(json) => Self::new(status, Content::Json(json), msg), - Err(e) => Self::unexpected(e), - } - } - - pub fn status(status: StatusCode) -> Self { - Self::new( - status, - Content::None, - status.canonical_reason().unwrap_or("").to_string(), - ) - } - - pub fn code(status: StatusCode, code: ErrorCode) -> Self { - Self::detail( - status, - code, - format!( - "standard {}: {}", - code as i64, - status.canonical_reason().unwrap_or("") - ), - ) - } - - pub fn catch_code<E: std::error::Error>(e: E, status: StatusCode, code: ErrorCode) -> Self { - Self::detail(status, code, format!("standard {}: {}", code as i64, e)) - } -} - -impl From<tokio_postgres::Error> for ServerError { - fn from(e: tokio_postgres::Error) -> Self { - ServerError::catch_code( - e, - StatusCode::BAD_GATEWAY, - ErrorCode::GENERIC_DB_FETCH_FAILED, - ) - } -} -pub trait CatchResult<T: Sized> { - fn unexpected(self) -> Result<T, ServerError>; - fn catch_code(self, status: StatusCode, code: ErrorCode) -> Result<T, ServerError>; -} - -impl<T, E: std::error::Error> CatchResult<T> for Result<T, E> { - fn unexpected(self) -> Result<T, ServerError> { - self.map_err(|e| ServerError::unexpected(e)) - } - - fn catch_code(self, status: StatusCode, code: ErrorCode) -> Result<T, ServerError> { - self.map_err(|e| ServerError::catch_code(e, status, code)) - } -} +/* + 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 common::{api_common::ErrorDetail, error_codes::ErrorCode}; +use hyper::{header, Body, Response, StatusCode}; + +/// Generic http error +#[derive(Debug)] +pub struct ServerError { + pub status: StatusCode, + content: Content, + pub msg: String, +} + +#[derive(Debug)] +enum Content { + None, + Json(Vec<u8>), +} + +impl ServerError { + fn new(status: StatusCode, content: Content, msg: String) -> Self { + Self { + status, + content, + msg, + } + } + + pub fn response(self) -> (Response<Body>, String) { + let builder = Response::builder().status(self.status); + let result = match self.content { + Content::None => builder.body(Body::empty()), + Content::Json(it) => builder + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(it)), + }; + (result.unwrap(), self.msg) + } + + pub fn unexpected<E: std::error::Error>(e: E) -> Self { + Self::new( + StatusCode::INTERNAL_SERVER_ERROR, + Content::None, + format!("unexpected: {}", e), + ) + } + + fn detail(status: StatusCode, code: ErrorCode, msg: String) -> Self { + let detail = ErrorDetail { + code: code as i64, + hint: None, + detail: None, + parameter: None, + path: None, + offset: None, + index: None, + object: None, + currency: None, + type_expected: None, + type_actual: None, + }; + match serde_json::to_vec(&detail) { + Ok(json) => Self::new(status, Content::Json(json), msg), + Err(e) => Self::unexpected(e), + } + } + + pub fn status(status: StatusCode) -> Self { + Self::new( + status, + Content::None, + status.canonical_reason().unwrap_or("").to_string(), + ) + } + + pub fn code(status: StatusCode, code: ErrorCode) -> Self { + Self::detail( + status, + code, + format!( + "standard {}: {}", + code as i64, + status.canonical_reason().unwrap_or("") + ), + ) + } + + pub fn catch_code<E: std::error::Error>(e: E, status: StatusCode, code: ErrorCode) -> Self { + Self::detail(status, code, format!("standard {}: {}", code as i64, e)) + } +} + +impl From<tokio_postgres::Error> for ServerError { + fn from(e: tokio_postgres::Error) -> Self { + ServerError::catch_code( + e, + StatusCode::BAD_GATEWAY, + ErrorCode::GENERIC_DB_FETCH_FAILED, + ) + } +} +pub trait CatchResult<T: Sized> { + fn unexpected(self) -> Result<T, ServerError>; + fn catch_code(self, status: StatusCode, code: ErrorCode) -> Result<T, ServerError>; +} + +impl<T, E: std::error::Error> CatchResult<T> for Result<T, E> { + fn unexpected(self) -> Result<T, ServerError> { + self.map_err(|e| ServerError::unexpected(e)) + } + + fn catch_code(self, status: StatusCode, code: ErrorCode) -> Result<T, ServerError> { + self.map_err(|e| ServerError::catch_code(e, status, code)) + } +}