From 80005a6b383eeaf4593a25173727ba5c03fa2e74 Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Tue, 28 Dec 2021 19:43:39 +0100 Subject: btc-wire: add and test bouncing --- btc-wire/src/bin/test.rs | 82 +++++++++++++++----- btc-wire/src/info.rs | 8 +- btc-wire/src/lib.rs | 38 ++++----- btc-wire/src/main.rs | 191 ++++++++++++++++++++++++++++++++-------------- btc-wire/src/rpc.rs | 36 +++++++-- script/setup.sh | 22 +++--- script/test_btc_fail.sh | 38 ++++++--- script/test_btc_stress.sh | 63 +++++++++++---- script/test_btc_wire.sh | 16 +++- script/test_recover_db.sh | 12 +-- test.conf | 2 +- 11 files changed, 352 insertions(+), 156 deletions(-) diff --git a/btc-wire/src/bin/test.rs b/btc-wire/src/bin/test.rs index f44495f..442e1a6 100644 --- a/btc-wire/src/bin/test.rs +++ b/btc-wire/src/bin/test.rs @@ -7,7 +7,6 @@ use btc_wire::{ rpc::{self, BtcRpc, Category}, rpc_utils::{default_data_dir, CLIENT, WIRE}, test::rand_key, - BounceErr, }; use owo_colors::OwoColorize; @@ -160,7 +159,7 @@ pub fn main() { // Send metadata let msg = "J'aime le chocolat".as_bytes(); let id = client_rpc - .send_op_return(&wire_addr, &test_amount, msg) + .send_op_return(&wire_addr, &test_amount, msg, false) .unwrap(); // Check in mempool assert!( @@ -204,7 +203,7 @@ pub fn main() { let before = client_rpc.get_balance().unwrap(); let send_id = client_rpc.send(&wire_addr, &test_amount, false).unwrap(); wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); - let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap(); + let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee, &[]).unwrap(); wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]); let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0] .fee @@ -227,27 +226,65 @@ pub fn main() { .send(&wire_addr, &Amount::from_sat(294), false) .unwrap(); wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); - assert!(match wire_rpc.bounce(&send_id, &bounce_fee) { - Ok(_) => false, - Err(err) => match err { - BounceErr::AmountLessThanFee => true, - _ => false, - }, + assert!(match wire_rpc.bounce(&send_id, &bounce_fee, &[]) { + Err(rpc::Error::RPC { + code: rpc::ErrorCode::RpcWalletInsufficientFunds, + msg: _, + }) => true, + _ => false, }); }); + runner.test("Bounce simple with metadata", || { + let before = client_rpc.get_balance().unwrap(); + let send_id = client_rpc.send(&wire_addr, &test_amount, false).unwrap(); + wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); + let bounce_id = wire_rpc + .bounce(&send_id, &bounce_fee, &[12, 34, 56, 78]) + .unwrap(); + wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]); + let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + let send_tx_fee = client_rpc.get_tx(&send_id).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + wire_rpc.get_tx_op_return(&bounce_id).unwrap(); + let after = client_rpc.get_balance().unwrap(); + assert!(before >= after); + assert_eq!(before - after, bounce_tx_fee + bounce_fee + send_tx_fee); + }); + runner.test("Bounce minimal amount with metadata", || { + let send_id = client_rpc + .send(&wire_addr, &Amount::from_sat(294), false) + .unwrap(); + wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); + assert!( + match wire_rpc.bounce(&send_id, &bounce_fee, &[12, 34, 56]) { + Err(rpc::Error::RPC { + code: rpc::ErrorCode::RpcWalletError, + .. + }) => true, + _ => false, + } + ); + }); runner.test("Bounce too small amount", || { let send_id = client_rpc .send(&wire_addr, &(Amount::from_sat(294) + bounce_fee), false) .unwrap(); wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); - - assert!(match wire_rpc.bounce(&send_id, &bounce_fee) { - Ok(_) => false, - Err(err) => match err { - BounceErr::RPC(rpc::Error::RPC { code, .. }) => - code == rpc::ErrorCode::RpcWalletInsufficientFunds, - _ => false, - }, + assert!(match wire_rpc.bounce(&send_id, &bounce_fee, &[]) { + Err(rpc::Error::RPC { + code: rpc::ErrorCode::RpcWalletInsufficientFunds, + .. + }) => true, + _ => false, }); }); runner.test("Bounce complex", || { @@ -260,7 +297,12 @@ pub fn main() { .into_iter() .map(|addresses| { client_rpc - .send_custom(&[], addresses.iter().map(|addr| (addr, &test_amount)), None) + .send_custom( + &[], + addresses.iter().map(|addr| (addr, &test_amount)), + None, + false, + ) .unwrap() }) .collect(); @@ -268,10 +310,10 @@ pub fn main() { let before = client_rpc.get_balance().unwrap(); // Send a transaction with multiple input from multiple transaction of different outputs len let send_id = client_rpc - .send_custom(&txs, [(&wire_addr, &(test_amount * 3))], None) + .send_custom(&txs, [(&wire_addr, &(test_amount * 3))], None, false) .unwrap(); wait_for_tx(&mut client_rpc, &mut reserve_rpc, &[send_id]); - let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap(); + let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee, &[]).unwrap(); wait_for_tx(&mut wire_rpc, &mut reserve_rpc, &[bounce_id]); let after = client_rpc.get_balance().unwrap(); let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().details[0] diff --git a/btc-wire/src/info.rs b/btc-wire/src/info.rs index db1a941..cc61b04 100644 --- a/btc-wire/src/info.rs +++ b/btc-wire/src/info.rs @@ -17,7 +17,7 @@ pub enum DecodeErr { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Info { Transaction { wtid: [u8; 32], url: Url }, - Bounce { id: Txid }, + Bounce { bounced: Txid }, } // We leave a potential special meaning for u8::MAX @@ -34,7 +34,7 @@ pub fn encode_info(info: &Info) -> Vec { buffer.extend_from_slice(&packed); return buffer; } - Info::Bounce { id } => { + Info::Bounce { bounced: id } => { buffer.push(BOUNCE_BYTE); buffer.extend_from_slice(id.as_ref()); } @@ -63,7 +63,7 @@ pub fn decode_info(bytes: &[u8]) -> Result { }) } BOUNCE_BYTE => Ok(Info::Bounce { - id: Txid::from_slice(&bytes[1..])?, + bounced: Txid::from_slice(&bytes[1..])?, }), unknown => Err(DecodeErr::UnknownFirstByte(unknown)), } @@ -100,7 +100,7 @@ mod test { for _ in 0..4 { let id = rand_key(); let info = Info::Bounce { - id: Txid::from_slice(&id).unwrap(), + bounced: Txid::from_slice(&id).unwrap(), }; let encode = encode_info(&info); let decoded = decode_info(&encode).unwrap(); diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs index 2317fa2..ed7d697 100644 --- a/btc-wire/src/lib.rs +++ b/btc-wire/src/lib.rs @@ -5,21 +5,11 @@ use rpc::{BtcRpc, Category, TransactionFull}; use rpc_utils::{segwit_min_amount, sender_address}; use segwit::{decode_segwit_msg, encode_segwit_key}; +pub mod config; pub mod rpc; pub mod rpc_utils; pub mod segwit; pub mod test; -pub mod config; - -#[derive(Debug, thiserror::Error)] -pub enum BounceErr { - #[error("Expected 'receive' transaction")] - NotAReceiveTransaction, - #[error("Transaction amount less than bounce fee")] - AmountLessThanFee, - #[error(transparent)] - RPC(#[from] rpc::Error), -} #[derive(Debug, thiserror::Error)] pub enum GetSegwitErr { @@ -92,10 +82,11 @@ impl BtcRpc { to: &Address, amount: &Amount, metadata: &[u8], + subtract_fee: bool, ) -> rpc::Result { assert!(metadata.len() > 0, "No medatata"); assert!(metadata.len() <= 80, "Max 80 bytes"); - self.send_custom(&[], [(to, amount)], Some(metadata)) + self.send_custom(&[], [(to, amount)], Some(metadata), subtract_fee) } /// Get detailed information about an in-wallet transaction and its op_return metadata @@ -124,22 +115,25 @@ impl BtcRpc { /// There is no reliable way to bounce a transaction as you cannot know if the addresses /// used are shared or come from a third-party service. We only send back to the first input /// address as a best-effort gesture. - pub fn bounce(&mut self, id: &Txid, bounce_fee: &Amount) -> Result { + pub fn bounce( + &mut self, + id: &Txid, + bounce_fee: &Amount, + metadata: &[u8], + ) -> Result { let full = self.get_tx(id)?; let detail = &full.details[0]; - if detail.category != Category::Receive { - return Err(BounceErr::NotAReceiveTransaction); - } + assert!(detail.category == Category::Receive); let amount = detail.amount.to_unsigned().unwrap(); - if amount <= *bounce_fee { - return Err(BounceErr::AmountLessThanFee); - } - let sender = sender_address(self, &full)?; - let bounce_amount = amount - *bounce_fee; + let bounce_amount = Amount::from_sat(amount.as_sat().saturating_sub(bounce_fee.as_sat())); // Send refund making recipient pay the transaction fees - let id = self.send(&sender, &bounce_amount, true)?; + let id = if metadata.is_empty() { + self.send(&sender, &bounce_amount, true)? + } else { + self.send_op_return(&sender, &bounce_amount, metadata, true)? + }; Ok(id) } } diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs index e728b7f..29ad7ca 100644 --- a/btc-wire/src/main.rs +++ b/btc-wire/src/main.rs @@ -1,7 +1,7 @@ use bitcoin::{hashes::Hash, Address, Amount as BtcAmount, BlockHash, SignedAmount, Txid}; use btc_wire::{ config::BitcoinConfig, - rpc::{BtcRpc, Category}, + rpc::{self, BtcRpc, Category, ErrorCode}, rpc_utils::{default_data_dir, sender_address}, segwit::DecodeSegWitErr, GetOpReturnErr, GetSegwitErr, @@ -24,7 +24,7 @@ use url::Url; use crate::{ fail_point::fail_point, info::{encode_info, Info}, - status::TxStatus, + status::{BounceStatus, TxStatus}, }; mod fail_point; @@ -68,7 +68,7 @@ fn last_hash(db: &mut Client) -> Result, postgres::Error> { /// Listen for new proposed transactions and announce them on the bitcoin network fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) { // Send a transaction on the blockchain, return true if more transactions with the same status remains - fn send_tx( + fn send( db: &mut Client, rpc: &mut BtcRpc, status: TxStatus, @@ -93,7 +93,7 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) let metadata = encode_info(&info); fail_point("Skip send_op_return", 0.2)?; - match rpc.send_op_return(&addr, &amount, &metadata) { + match rpc.send_op_return(&addr, &amount, &metadata, false) { Ok(tx_id) => { fail_point("Fail update db", 0.2)?; tx.execute( @@ -101,7 +101,7 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) &[&(TxStatus::Sent as i16), &tx_id.as_ref(), &id], )?; let amount = btc_amount_to_taler_amount(&amount.to_signed().unwrap()); - info!("SEND >> {} {} in {}", addr, amount, tx_id); + info!("send {} {} in {}", addr, amount, tx_id); } Err(e) => { info!("sender: RPC - {}", e); @@ -116,6 +116,54 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) Ok(row.is_some()) } + // Bounce a transaction on the blockchain, return true if more bounce with the same status remains + fn bounce( + db: &mut Client, + rpc: &mut BtcRpc, + status: BounceStatus, + fee: &BtcAmount, + ) -> Result> { + assert!(status == BounceStatus::Delayed || status == BounceStatus::Requested); + let mut tx = db.transaction()?; + // We lock the row with FOR UPDATE to prevent sending same transaction multiple time + let row = tx.query_opt( + "SELECT id, bounced FROM bounce WHERE status=$1 LIMIT 1 FOR UPDATE", + &[&(status as i16)], + )?; + if let Some(row) = &row { + let id: i32 = row.get(0); + let bounced: Txid = Txid::from_slice(row.get(1))?; + let info = Info::Bounce { bounced }; + let metadata = encode_info(&info); + + fail_point("Skip send_op_return", 0.2)?; + match rpc.bounce(&bounced, &fee, &metadata) { + Ok(it) => { + info!("bounce {} in {}", &bounced, &it); + tx.execute( + "UPDATE bounce SET txid = $1, status = $2 WHERE id = $3", + &[&it.as_ref(), &(BounceStatus::Sent as i16), &id], + )?; + } + Err(err) => match err { + rpc::Error::RPC { + code: ErrorCode::RpcWalletInsufficientFunds | ErrorCode::RpcWalletError, + msg, + } => { + info!("ignore bounce {} because {}", &bounced, msg); + tx.execute( + "UPDATE bounce SET status = $1 WHERE id = $2", + &[&(BounceStatus::Ignored as i16), &id], + )?; + } + _ => Err(err)?, + }, + } + tx.commit()?; + } + Ok(row.is_some()) + } + // TODO check if transactions are abandoned let mut failed = false; @@ -143,29 +191,15 @@ fn worker(mut rpc: AutoReconnectRPC, mut db: AutoReconnectSql, config: &Config) // As we are now in sync with the blockchain if a transaction is in requested or delayed state it have not been sent // Send delayed transactions - while send_tx(db, rpc, TxStatus::Delayed)? {} + while send(db, rpc, TxStatus::Delayed)? {} // Send requested transactions - while send_tx(db, rpc, TxStatus::Requested)? {} + while send(db, rpc, TxStatus::Requested)? {} - // Check if already bounced - /*if nb > 0 && false { - // We do not handle failures, bouncing is done in a best effort manner - match rpc.bounce(&id, &BtcAmount::from_sat(config.bounce_fee)) { - Ok(it) => { - info!("bounce {} in {}", &id, &it); - db.execute( - "UPDATE bounce SET txid = $1 WHERE bounced = $2", - &[&it.as_ref(), &id.as_ref()], - )?; - } - Err(err) => match err { - BounceErr::AmountLessThanFee => { /* Ignore */ } - BounceErr::NotAReceiveTransaction | BounceErr::RPC(_) => { - Err(err)? - } - }, - } - }*/ + let bounce_fee = BtcAmount::from_sat(config.bounce_fee); + // Send delayed bounce + while bounce(db, rpc, BounceStatus::Delayed, &bounce_fee)? {} + // Send requested bounce + while bounce(db, rpc, BounceStatus::Requested, &bounce_fee)? {} Ok(()) })(); @@ -186,22 +220,22 @@ fn sync_chain( ) -> Result<(), Box> { // Get stored last_hash let last_hash = last_hash(db)?; - let confirmation = config.confirmation; + let min_confirmations = config.confirmation; // Get a set of transactions ids to parse - let (txs, lastblock): (HashMap, BlockHash) = { + let (txs, lastblock): (HashMap, BlockHash) = { // Get all transactions made since this block - let list = rpc.list_since_block(last_hash.as_ref(), confirmation, true)?; + let list = rpc.list_since_block(last_hash.as_ref(), min_confirmations, true)?; // Only keep ids and category - let txs: HashMap = list + let txs = list .transactions .into_iter() - .map(|tx| (tx.txid, tx.category)) + .map(|tx| (tx.txid, (tx.category, tx.confirmations))) .collect(); (txs, list.lastblock) }; - for (id, category) in txs { + for (id, (category, confirmations)) in txs { match category { Category::Send => { match rpc.get_tx_op_return(&id) { @@ -211,14 +245,15 @@ fn sync_chain( Ok(info) => match info { Info::Transaction { wtid, url } => { let row = tx.query_opt( - "SELECT status, id FROM tx_out WHERE wtid=$1 FOR UPDATE", + "SELECT id, status FROM tx_out WHERE wtid=$1 FOR UPDATE", &[&wtid.as_ref()], )?; if let Some(row) = row { - let status: i16 = row.get(0); - let _id: i32 = row.get(1); + let _id: i32 = row.get(0); + let status: i16 = row.get(1); let status: TxStatus = TxStatus::try_from(status as u8).unwrap(); + // TODO match if status != TxStatus::Sent { tx.execute( "UPDATE tx_out SET status=$1 where id=$2", @@ -235,7 +270,7 @@ fn sync_chain( full.details[0].address.as_ref().unwrap(); let amount = btc_amount_to_taler_amount(&full.amount); - info!("SEND >> {} {} in {}", addr, amount, &id); + info!("send {} {} in {}", addr, amount, &id); } } } else { @@ -249,16 +284,53 @@ fn sync_chain( OsRng.fill_bytes(&mut request_uid); let nb = tx.execute( "INSERT INTO tx_out (_date, amount, wtid, debit_acc, credit_acc, exchange_url, status, txid, request_uid) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (wtid) DO NOTHING", - &[&date, &amount.to_string(), &wtid.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref(), &config.base_url.as_ref(), &(TxStatus::Sent as i16), &id.as_ref(), &request_uid.as_ref() - ], - )?; + &[&date, &amount.to_string(), &wtid.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref(), &config.base_url.as_ref(), &(TxStatus::Sent as i16), &id.as_ref(), &request_uid.as_ref()], + )?; if nb > 0 { - warn!("watcher: found an unregistered outgoing address {} {} in tx {}", crockford_base32_encode(&wtid), &url, id); + warn!( + "recovered {} {} in tx {}", + crockford_base32_encode(&wtid), + &url, + id + ); } } } - Info::Bounce { .. } => { - // TODO + Info::Bounce { bounced } => { + let row = tx.query_opt( + "SELECT id, status FROM bounce WHERE bounced=$1 FOR UPDATE", + &[&bounced.as_ref()], + )?; + if let Some(row) = row { + let _id: i32 = row.get(0); + let status: i16 = row.get(1); + let status: BounceStatus = + BounceStatus::try_from(status as u8).unwrap(); + assert!(status != BounceStatus::Ignored); // TODO + if status != BounceStatus::Sent { + tx.execute( + "UPDATE bounce SET status=$1 where id=$2", + &[&(BounceStatus::Sent as i16), &_id], + )?; + if status == BounceStatus::Delayed + || status == BounceStatus::Requested + { + warn!( + "watcher: bounce {} have been recovered automatically", + _id + ); + info!("bounce {} in {}", &bounced, &id); + } + } + } else { + let nb = tx.execute( + "INSERT INTO bounce (bounced, txid, status) VALUES ($1, $2, $3) ON CONFLICT (txid) DO NOTHING", + &[&bounced.as_ref(), &id.as_ref(), &(BounceStatus::Sent as i16)], + )?; + if nb > 0 { + info!("recovered bounce {} in {}", &bounced, &id); + } + } } }, Err(err) => warn!("send: decode-info {} - {}", id, err), @@ -271,9 +343,9 @@ fn sync_chain( }, } } - Category::Receive => match rpc.get_tx_segwit_key(&id) { - Ok((full, reserve_pub)) => { - if full.confirmations >= confirmation as i32 { + Category::Receive if confirmations >= min_confirmations as i32 => { + match rpc.get_tx_segwit_key(&id) { + Ok((full, reserve_pub)) => { let debit_addr = sender_address(rpc, &full)?; let credit_addr = full.details[0].address.as_ref().unwrap(); let date = SystemTime::UNIX_EPOCH + Duration::from_secs(full.time); @@ -282,22 +354,25 @@ fn sync_chain( &date, &amount.to_string(), &reserve_pub.as_ref(), &btc_payto_url(&debit_addr).as_ref(), &btc_payto_url(credit_addr).as_ref() ])?; if nb > 0 { - info!("{} << {} {} in {}", &debit_addr, &credit_addr, &amount, &id); + info!( + "receive {} << {} {} in {}", + &debit_addr, &credit_addr, &amount, &id + ); } } + Err(err) => match err { + GetSegwitErr::Decode( + DecodeSegWitErr::MissingSegWitAddress | DecodeSegWitErr::NoMagicIdMatch, + ) => { + // Request a bounce + db.execute("INSERT INTO bounce (bounced) VALUES ($1) ON CONFLICT (bounced) DO NOTHING", &[&id.as_ref()])?; + } + err => warn!("receive: {} {}", id, err), + }, } - Err(err) => match err { - GetSegwitErr::Decode( - DecodeSegWitErr::MissingSegWitAddress | DecodeSegWitErr::NoMagicIdMatch, - ) => { - // Request a bounce - db.execute("INSERT INTO bounce (bounced) VALUES ($1) ON CONFLICT (bounced) DO NOTHING", &[&id.as_ref()])?; - } - err => warn!("receive: {} {}", id, err), - }, - }, - Category::Generate | Category::Immature | Category::Orphan => { - // Ignore coinbase transactions + } + _ => { + // Ignore coinbase and unconfirmed send transactions } } } diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs index 8121e1e..bac9a6b 100644 --- a/btc-wire/src/rpc.rs +++ b/btc-wire/src/rpc.rs @@ -206,7 +206,13 @@ impl BtcRpc { inputs: impl IntoIterator, outputs: impl IntoIterator, data: Option<&[u8]>, + subtract_fee: bool, ) -> Result { + let mut outputs: Vec = outputs + .into_iter() + .map(|(addr, amount)| json!({&addr.to_string(): amount.as_btc()})) + .collect(); + let len = outputs.len(); let hex: String = self.call( "createrawtransaction", &[ @@ -218,18 +224,26 @@ impl BtcRpc { .collect(), ), Value::Array({ - let mut vec: Vec = outputs - .into_iter() - .map(|(addr, amount)| json!({&addr.to_string(): amount.as_btc()})) - .collect(); if let Some(data) = data { - vec.push(json!({ "data".to_string(): data.to_hex() })); + outputs.push(json!({ "data".to_string(): data.to_hex() })); } - vec + outputs }), ], )?; - let funded: HexWrapper = self.call("fundrawtransaction", &[hex])?; + let funded: HexWrapper = self.call( + "fundrawtransaction", + &( + hex, + FundOption { + subtract_fee_from_outputs: if subtract_fee { + (0..len).into_iter().collect() + } else { + vec![] + }, + }, + ), + )?; let signed: HexWrapper = self.call("signrawtransactionwithwallet", &[&funded.hex])?; self.call("sendrawtransaction", &[&signed.hex]) } @@ -252,7 +266,13 @@ impl BtcRpc { } } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FundOption { + pub subtract_fee_from_outputs: Vec, +} + +#[derive(Debug, serde::Deserialize)] pub struct Wallet { pub name: String, pub warning: Option, diff --git a/script/setup.sh b/script/setup.sh index 827ff56..5ef9d1e 100644 --- a/script/setup.sh +++ b/script/setup.sh @@ -46,7 +46,6 @@ function setup_btc() { WIRE=`$BTC_CLI -rpcwallet=wire getnewaddress` mine_btc 101 $BTC_CLI -rpcwallet=reserve sendtoaddress $CLIENT 10 > /dev/null - $BTC_CLI -rpcwallet=reserve sendtoaddress $WIRE 1 > /dev/null mine_btc } @@ -67,10 +66,14 @@ function next_btc() { # Check client and wire balance function check_balance() { - CLIENT_BALANCE=`$BTC_CLI -rpcwallet=client getbalance` - WIRE_BALANCE=`$BTC_CLI -rpcwallet=wire getbalance` - if [ "$CLIENT_BALANCE" != "$1" ] || [ "$WIRE_BALANCE" != "${2:-$WIRE_BALANCE}" ]; then - echo "expected: client $1 wire ${2:-$WIRE_BALANCE} got: client $CLIENT_BALANCE wire $WIRE_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 } @@ -109,16 +112,17 @@ function gateway() { # 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 BTC:0.0000$n > /dev/null`; then - echo -n " missing tx with amount: BTC:0.0000$n" + if ! `echo $ALL | grep BTC:$PRE$n > /dev/null`; then + echo -n " missing tx with amount: BTC:$PRE$n" return 1 fi done - NB=`echo $ALL | grep -o BTC:0.0000 | wc -l` + NB=`echo $ALL | grep -o BTC:$PRE | wc -l` EXPECTED=`$2 | wc -w` if [ "$EXPECTED" != "$NB" ]; then echo -n " expected: $EXPECTED txs found $NB" return 1 fi -} \ No newline at end of file +} diff --git a/script/test_btc_fail.sh b/script/test_btc_fail.sh index 3602364..c9fa930 100644 --- a/script/test_btc_fail.sh +++ b/script/test_btc_fail.sh @@ -35,26 +35,22 @@ echo "" SEQ="seq 10 40" -function check() { - check_delta "$1?delta=-100" "$SEQ" -} - echo "----- Handle incoming -----" echo -n "Making wire transfer to exchange:" for n in `$SEQ`; do - btc-wire-cli -d $BTC_DIR transfer 0.0000$n + btc-wire-cli -d $BTC_DIR transfer 0.000$n mine_btc # Mine transactions done next_btc # Trigger btc_wire echo " OK" echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Check balance:" -check_balance 9.99897979 1.00077500 +check_balance 9.99200479 0.00775000 echo " OK" echo "----- Handle outgoing -----" @@ -66,14 +62,34 @@ for n in `$SEQ`; do -C payto://bitcoin/$CLIENT \ -a BTC:0.0000$n > /dev/null done -sleep 15 +sleep 20 mine_btc # Mine transactions echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" +echo " OK" + +echo -n "Check balance:" +check_balance 9.99277979 0.00691331 +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 +sleep 20 echo " OK" echo -n "Check balance:" -check_balance 9.99975479 -echo " OK" \ No newline at end of file +check_balance "*" 0.00031000 +echo " OK" + +echo "All tests passed" \ No newline at end of file diff --git a/script/test_btc_stress.sh b/script/test_btc_stress.sh index b3b6ed7..6b6023f 100644 --- a/script/test_btc_stress.sh +++ b/script/test_btc_stress.sh @@ -28,22 +28,18 @@ init_btc echo "Init bitcoin regtest" setup_btc echo "Start btc-wire stressed" -stressed_btc_wire +btc_wire echo "Start gateway" gateway echo "" SEQ="seq 10 99" -function check() { - check_delta "$1?delta=-100" "$SEQ" -} - echo "----- Handle incoming -----" echo -n "Making wire transfer to exchange:" for n in `$SEQ`; do - btc-wire-cli -d $BTC_DIR transfer 0.0000$n + btc-wire-cli -d $BTC_DIR transfer 0.000$n mine_btc # Mine transactions done sleep 3 # Give time for btc_wire worker to process @@ -52,11 +48,11 @@ sleep 3 # Give time for btc_wire worker to process echo " OK" echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Check balance:" -check_balance 9.99438310 1.00490500 +check_balance 9.95023810 0.04905000 echo " OK" echo "----- Handle outgoing -----" @@ -73,32 +69,71 @@ next_btc # Mine transactions echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" echo " OK" echo -n "Check balance:" -check_balance 9.99928810 +check_balance 9.95514310 echo " OK" next_btc # Mine transactions + +echo "----- Recover DB -----" + +echo "Reset database" +reset_db # Clear database tables +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.95514310 +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 +sleep 20 +echo " OK" + +echo -n "Check balance:" +check_balance "*" 0.00090000 +echo " OK" + echo "----- Recover DB -----" echo "Reset database" reset_db # Clear database tables mine_btc # Trigger worker -sleep 3 +sleep 10 echo -n "Requesting exchange incoming transaction list:" -check incoming +check_delta "incoming?delta=-100" "$SEQ" "0.000" echo " OK" echo -n "Requesting exchange outgoing transaction list:" -check outgoing +check_delta "outgoing?delta=-100" "$SEQ" echo " OK" echo -n "Check balance:" # Balance should not have changed -check_balance 9.99928810 +check_balance "*" 0.00090000 echo " OK" echo "All tests passed" \ No newline at end of file diff --git a/script/test_btc_wire.sh b/script/test_btc_wire.sh index 7795a0e..442dcf3 100644 --- a/script/test_btc_wire.sh +++ b/script/test_btc_wire.sh @@ -33,25 +33,33 @@ echo "Start gateway" gateway echo "" -echo "---- Gateway API -----" +echo "---- Receive -----" echo -n "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.00004 next_btc -check_balance 9.99995209 1.00004000 +check_balance 9.99995209 0.00004000 echo " OK" echo -n "Requesting exchange incoming transaction list:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep BTC:0.00004 > /dev/null echo " OK" +echo "---- Bounce-----" +$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.00042 +next_btc +check_balance 9.99993883 0.00005000 + +echo "---- Send -----" + echo -n "Making wire transfer from exchange:" taler-exchange-wire-gateway-client \ -b $BANK_ENDPOINT \ -C payto://bitcoin/$CLIENT \ -a BTC:0.00002 > /dev/null -next_btc -check_balance 9.99997209 1.00001801 +sleep 0.3 +mine_btc +check_balance 9.99995883 0.00002801 echo " OK" echo -n "Requesting exchange's outgoing transaction list:" diff --git a/script/test_recover_db.sh b/script/test_recover_db.sh index 3bfc9f9..d90a541 100644 --- a/script/test_recover_db.sh +++ b/script/test_recover_db.sh @@ -37,7 +37,7 @@ echo "----- With DB -----" echo "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.000042 next_btc -check_balance 9.99995009 1.00004200 +check_balance 9.99995009 0.00004200 echo -n "Requesting exchange incoming transaction list:" taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep BTC:0.000042 > /dev/null && echo " OK" || echo " Failed" @@ -45,10 +45,12 @@ echo "----- Without DB -----" echo "Stop database" sudo service postgresql stop > /dev/null +echo "Making incomplete wire transfer to exchange" +$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.00042 &> /dev/null echo "Making wire transfer to exchange:" btc-wire-cli -d $BTC_DIR transfer 0.00004 next_btc -check_balance 9.99990218 1.00008200 +check_balance 9.99948077 0.00050200 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" @@ -65,8 +67,8 @@ taler-exchange-wire-gateway-client \ -C payto://bitcoin/$CLIENT \ -a BTC:0.00002 > /dev/null sleep 1 -next_btc -check_balance 9.99992218 1.00006001 +mine_btc +check_balance 9.99990892 0.00007001 echo " OK" echo -n "Requesting exchange's outgoing transaction list:" @@ -95,6 +97,6 @@ done echo "" # Balance should not have changed -check_balance 9.99992218 1.00006001 +check_balance 9.99990892 0.00007001 echo "All tests passed" \ No newline at end of file diff --git a/test.conf b/test.conf index d3f7053..e59092f 100644 --- a/test.conf +++ b/test.conf @@ -9,4 +9,4 @@ PAYTO = payto://bitcoin/bcrt1qgkgxkjj27g3f7s87mcvjjsghay7gh34cx39prj CONFIRMATION = 1 BTC_WALLET = wire BTC_DATA_DIR = ~/.bitcoin -BOUNCE_FEE = 0 \ No newline at end of file +BOUNCE_FEE = 1000 \ No newline at end of file -- cgit v1.2.3