depolymerization

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

commit 58b69e0cb114272dcbd31a8d43727169a6104560
parent 579cd18c2560716b4924a73ec097720731745502
Author: Antoine A <>
Date:   Tue, 11 Jan 2022 14:58:06 +0100

btc-wire: support and test incoming transactions conflicting reorganisation

Diffstat:
Mbtc-wire/src/bin/btc-wire-cli.rs | 8++++----
Mbtc-wire/src/main.rs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mmakefile | 1+
Mscript/setup.sh | 33+++++++++++++++++++++++----------
Mscript/test_btc_conflict.sh | 4----
Ascript/test_btc_hell.sh | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscript/test_btc_reconnect.sh | 2+-
Mscript/test_btc_reorg.sh | 4++--
8 files changed, 233 insertions(+), 38 deletions(-)

diff --git a/btc-wire/src/bin/btc-wire-cli.rs b/btc-wire/src/bin/btc-wire-cli.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use bitcoin::{Address, Amount, Network}; use btc_wire::{ config::BitcoinConfig, - rpc::{BtcRpc, Error, ErrorCode, Category}, + rpc::{BtcRpc, Category, Error, ErrorCode}, rpc_utils::default_data_dir, test::rand_key, }; @@ -51,7 +51,7 @@ struct TransferCmd { #[argh(subcommand, name = "nblock")] /// Wait or mine the next block struct NextBlockCmd { - #[argh(option, short = 't', default = "String::from(\"wire\")")] + #[argh(positional, default = "String::from(\"wire\")")] /// receiver wallet to: String, } @@ -60,7 +60,7 @@ struct NextBlockCmd { #[argh(subcommand, name = "abandon")] /// Abandon all unconfirmed transaction struct AbandonCmd { - #[argh(option, short = 't', default = "String::from(\"wire\")")] + #[argh(positional, default = "String::from(\"wire\")")] /// sender wallet from: String, } @@ -133,7 +133,7 @@ fn main() { let (mut wire, _) = app.auto_wallet(&from); let list = wire.list_since_block(None, 1, false).unwrap(); for tx in list.transactions { - if tx.category == Category::Send && tx.confirmations == 0 { + if tx.category == Category::Send && tx.confirmations == 0 { wire.abandon_tx(&tx.txid).unwrap(); } } diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs @@ -3,7 +3,6 @@ use btc_wire::{ config::BitcoinConfig, rpc::{self, BtcRpc, Category, ErrorCode}, rpc_utils::{default_data_dir, sender_address}, - segwit::DecodeSegWitErr, GetOpReturnErr, GetSegwitErr, }; use info::decode_info; @@ -12,6 +11,7 @@ use rand::{rngs::OsRng, RngCore}; use reconnect::{AutoReconnectRPC, AutoReconnectSql}; use std::{ collections::{HashMap, HashSet}, + fmt::Write, process::exit, str::FromStr, time::{Duration, SystemTime}, @@ -235,23 +235,65 @@ fn sync_chain( }; // Check if a confirmed incoming transaction have been removed by a blockchain reorganisation + if !removed.is_empty() { + let mut blocking_receive = Vec::new(); + let mut blocking_bounce = Vec::new(); for id in removed { - if let Ok((full, key)) = rpc.get_tx_segwit_key(&id) { - // If the removed tx is not in confirmed the txs list and the tx is stored in the database hard error - if txs - .get(&id) - .map(|(_, confirmations)| *confirmations < min_confirmations as i32) - .unwrap_or(true) - && db - .query_opt("SELECT 1 FROM tx_in WHERE reserve_pub=$1", &[&key.as_ref()])? - .is_some() - { - let credit_addr = full.details[0].address.as_ref().unwrap(); - error!("Received transaction {} in {} from {} have been removed from the blockchain, bitcoin backing is compromised until the transaction reappear", base32(&key), id, credit_addr); - exit(1); + match rpc.get_tx_segwit_key(&id) { + Ok((full, key)) => { + // If the removed tx is not in confirmed the txs list and the tx is stored in the database hard error + if txs + .get(&id) + .map(|(_, confirmations)| *confirmations < min_confirmations as i32) + .unwrap_or(true) + && db + .query_opt( + "SELECT 1 FROM tx_in WHERE reserve_pub=$1", + &[&key.as_ref()], + )? + .is_some() + { + let debit_addr = sender_address(rpc, &full)?; + blocking_receive.push((key, id, debit_addr)); + } } + Err(err) => { + match err { + GetSegwitErr::Decode(_) => { + if let Some(row) = db.query_opt( + "SELECT txid FROM bounce WHERE bounced=$1 AND txid IS NOT NULL", + &[&id.as_ref()], + )? { + let txid = Txid::from_slice(row.get(0)).unwrap(); + blocking_bounce.push((txid, id)); + } else { + db.execute("DELETE FROM bounce WHERE bounced=$1", &[&id.as_ref()])?; + } + } + _ => { /* ignore already caught error */ } + } + } + } + } + + if !blocking_bounce.is_empty() || !blocking_receive.is_empty() { + let mut buf = "The following transaction have been removed from the blockchain, bitcoin backing is compromised until the transaction reappear:".to_string(); + for (key, id, addr) in blocking_receive { + write!( + &mut buf, + "\n\treceived {} in {} from {}", + base32(&key), + id, + addr + ) + .unwrap(); + } + for (id, bounced) in blocking_bounce { + write!(&mut buf, "\n\tbounced {} in {}", id, bounced).unwrap(); } + error!("{}", buf); + exit(1); } } @@ -404,9 +446,7 @@ fn sync_chain( } } Err(err) => match err { - GetSegwitErr::Decode( - DecodeSegWitErr::MissingSegWitAddress | DecodeSegWitErr::NoMagicIdMatch, - ) => { + GetSegwitErr::Decode(_) => { // Request a bounce db.execute("INSERT INTO bounce (bounced) VALUES ($1) ON CONFLICT (bounced) DO NOTHING", &[&id.as_ref()])?; } diff --git a/makefile b/makefile @@ -13,5 +13,6 @@ test_btc: script/test_btc_stress.sh script/test_btc_conflict.sh script/test_btc_reorg.sh + script/test_btc_hell.sh test: test_gateway test_btc \ No newline at end of file diff --git a/script/setup.sh b/script/setup.sh @@ -27,6 +27,9 @@ for dir in $BTC_DIR $BTC_DIR2 $DB_DIR log; do mkdir -p $dir done +# Clear logs +rm log/* + # Setup command helpers BTC_CLI="bitcoin-cli -datadir=$BTC_DIR" BTC_CLI2="bitcoin-cli -datadir=$BTC_DIR2" @@ -78,7 +81,7 @@ function reset_db() { # Start a bitcoind regtest node, generate money, wallet and addresses function init_btc() { cp ${BASH_SOURCE%/*}/conf/bitcoin.conf $BTC_DIR/bitcoin.conf - bitcoind -datadir=$BTC_DIR $* &> log/btc.log & + bitcoind -datadir=$BTC_DIR $* &>> log/btc.log & BTC_PID="$!" # Wait for RPC server to be online $BTC_CLI -rpcwait getnetworkinfo > /dev/null @@ -99,7 +102,7 @@ function init_btc() { # Start a second bitcoind regtest node connected to the first one function init_btc2() { cp ${BASH_SOURCE%/*}/conf/bitcoin2.conf $BTC_DIR2/bitcoin.conf - bitcoind -datadir=$BTC_DIR2 $* &> log/btc2.log & + bitcoind -datadir=$BTC_DIR2 $* &>> log/btc2.log & $BTC_CLI2 -rpcwait getnetworkinfo > /dev/null $BTC_CLI addnode 127.0.0.1:8346 onetry } @@ -116,21 +119,31 @@ function btc2_fork() { sleep 1 } -# Start a bitcoind regest server in a previously created temporary directory and load wallets -function restart_btc() { +# Restart a bitcoind regest server in a previously created temporary directory and load wallets +function resume_btc() { + # Restart node bitcoind -datadir=$BTC_DIR $* &>> log/btc.log & BTC_PID="$!" + # Wait for RPC server to be online $BTC_CLI -rpcwait getnetworkinfo > /dev/null + # Load wallets for wallet in wire client reserve; do $BTC_CLI loadwallet $wallet > /dev/null done + # Connect second node + $BTC_CLI addnode 127.0.0.1:8346 onetry } function stop_btc() { - kill $BTC_PID + kill $BTC_PID wait $BTC_PID } +function restart_btc() { + stop_btc + resume_btc $* +} + # Mine blocks function mine_btc() { $BTC_CLI generatetoaddress "${1:-1}" $RESERVE > /dev/null @@ -171,22 +184,22 @@ function check_balance() { # Start btc_wire function btc_wire() { cargo build --bin btc-wire --release &> /dev/null - target/release/btc-wire $CONF &> log/btc_wire.log & + target/release/btc-wire $CONF &>> log/btc_wire.log & WIRE_PID="$!" } # Start btc_wire with random failures function fail_btc_wire() { cargo build --bin btc-wire --release --features fail &> /dev/null - target/release/btc-wire $CONF &> log/btc_wire.log & + target/release/btc-wire $CONF &>> log/btc_wire.log & WIRE_PID="$!" } # Start multiple btc_wire in parallel function stressed_btc_wire() { cargo build --bin btc-wire --release &> /dev/null - target/release/btc-wire $CONF &> log/btc_wire.log & - target/release/btc-wire $CONF &> log/btc_wire1.log & + target/release/btc-wire $CONF &>> log/btc_wire.log & + target/release/btc-wire $CONF &>> log/btc_wire1.log & } # ----- Gateway ------ # @@ -194,7 +207,7 @@ function stressed_btc_wire() { # Start wire_gateway in test mode function gateway() { cargo build --bin wire-gateway --release --features test &> /dev/null - target/release/wire-gateway $CONF &> log/gateway.log & + target/release/wire-gateway $CONF &>> log/gateway.log & GATEWAY_PID="$!" for n in `seq 1 50`; do echo -n "." diff --git a/script/test_btc_conflict.sh b/script/test_btc_conflict.sh @@ -40,7 +40,6 @@ check_balance 9.95799209 0.03799801 echo " OK" echo -n "Abandon pending transaction:" -stop_btc restart_btc -minrelaytxfee=0.0001 btc-wire-cli -d $BTC_DIR abandon check_balance 9.95799209 0.04200000 @@ -52,7 +51,6 @@ taler-exchange-wire-gateway-client \ -C payto://bitcoin/$CLIENT \ -a BTC:0.005 > /dev/null sleep 1 -stop_btc restart_btc mine_btc check_balance 9.96299209 0.03698010 @@ -93,7 +91,6 @@ check_balance 9.95999859 0.00001000 echo " OK" echo -n "Abandon pending transaction:" -stop_btc restart_btc -minrelaytxfee=0.0001 btc-wire-cli -d $BTC_DIR abandon check_balance 9.95999859 0.04000000 @@ -103,7 +100,6 @@ echo -n "Generate conflict:" $BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.05 > /dev/null mine_btc $CONFIRMATION sleep 1 -stop_btc restart_btc mine_btc check_balance 9.95994929 0.04001000 diff --git a/script/test_btc_hell.sh b/script/test_btc_hell.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +## Test btc_wire correctness when a blockchain reorganisation occurs leading to past incoming transaction conflict + +set -eu + +source "${BASH_SOURCE%/*}/setup.sh" +SCHEMA=btc.sql + +echo "----- Setup -----" +echo "Load config file" +load_config +echo "Start database" +setup_db +echo "Start bitcoin node" +init_btc +echo "Start second bitcoin node" +init_btc2 +echo "Start btc-wire" +btc_wire +echo "Start gateway" +gateway +echo "" + +# Check btc-wire is running +function up() { + check_up $WIRE_PID btc_wire +} +# Check btc-wire is not running +function down() { + check_down $WIRE_PID btc_wire +} + +echo "----- Handle reorg conflicting incoming receive -----" + +echo "Loose second bitcoin node" +btc2_deco + +echo -n "Gen incoming transactions:" +btc-wire-cli -d $BTC_DIR 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:" +up +btc2_fork +check_balance 9.99579209 0.00000000 +down +echo " OK" + +echo -n "Check btc-wire hard error on restart:" +btc_wire +sleep 1 +down +echo " OK" + +echo -n "Generate conflict:" +restart_btc -minrelaytxfee=0.0001 +btc-wire-cli -d $BTC_DIR abandon client +btc-wire-cli -d $BTC_DIR transfer 0.0054 > /dev/null +next_btc +check_balance 9.99457382 0.00540000 +echo " OK" + +echo -n "Check btc-wire never heal on restart:" +btc_wire +sleep 1 +down +check_balance 9.99457382 0.00540000 +echo " OK" + +echo -n "Check btc-wire have not read the conflicting transaction:" +check_delta "incoming" "" +echo " OK" + +# Recover by paying for the customer ? + +echo "----- Reset -----" +echo "Cleanup" +cleanup +source "${BASH_SOURCE%/*}/setup.sh" +echo "Load config file" +load_config +echo "Start database" +setup_db +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" +btc2_deco + +echo -n "Generate bounce:" +$BTC_CLI -rpcwallet=client sendtoaddress $WIRE 0.042 > /dev/null +next_btc +sleep 1 +check_balance 9.99998674 0.00001000 +echo " OK" + +echo -n "Perform fork and check btc-wire hard error:" +up +btc2_fork +check_balance 9.95799859 0.00000000 +down +echo " OK" + +echo -n "Check btc-wire hard error on restart:" +btc_wire +sleep 1 +down +echo " OK" + +echo -n "Generate conflict:" +restart_btc -minrelaytxfee=0.0001 +btc-wire-cli -d $BTC_DIR abandon client +btc-wire-cli -d $BTC_DIR transfer 0.054 > /dev/null +next_btc +check_balance 9.94597382 0.05400000 +echo " OK" + +sleep 5 + +echo -n "Check btc-wire never heal on restart:" +btc_wire +sleep 1 +down +check_balance 9.94597382 0.05400000 +echo " OK" + +echo -n "Check btc-wire have not read the conflicting transaction:" +check_delta "incoming" "" +echo " OK" + + +echo "All tests passed" +\ No newline at end of file diff --git a/script/test_btc_reconnect.sh b/script/test_btc_reconnect.sh @@ -49,7 +49,7 @@ echo "----- Reconnect DB -----" echo "Start database" pg_ctl start -D $DB_DIR > /dev/null echo "Start bitcoin node" -restart_btc +resume_btc sleep 6 # Wait for connection to be available 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 " Failed" diff --git a/script/test_btc_reorg.sh b/script/test_btc_reorg.sh @@ -119,11 +119,11 @@ sleep 1 check_balance "*" 0.00011000 echo " OK" -echo -n "Perform fork and check btc-wire still up:" +echo -n "Perform fork and check btc-wire hard error:" up btc2_fork check_balance "*" 0.00000000 -up +down echo " OK" echo -n "Recover orphaned transactions:"