depolymerization

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

commit f8e660fa3915e3b7f4320dda808db582d20e6de6
parent 60e4b9be19afea1a91e0b29cf4ff14af6f41dbfa
Author: Antoine A <>
Date:   Thu, 10 Feb 2022 15:40:15 +0100

eth-wire: add analysis

Diffstat:
Mbtc-wire/src/loops/analysis.rs | 12++++++------
Mbtc-wire/src/main.rs | 14+-------------
Mcommon/src/lib.rs | 15+++++++++++++++
Meth-wire/src/loops.rs | 1+
Aeth-wire/src/loops/analysis.rs | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meth-wire/src/main.rs | 12+++++++++---
Mmakefile | 2++
Atest/eth/analysis.sh | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 198 insertions(+), 22 deletions(-)

diff --git a/btc-wire/src/loops/analysis.rs b/btc-wire/src/loops/analysis.rs @@ -38,7 +38,7 @@ pub fn analysis(mut rpc: AutoReconnectRPC, mut db: AutoReconnectDb, state: &Wire db.batch_execute("LISTEN new_block")?; loop { // Get biggest known valid fork - let max_fork = rpc + let fork = rpc .get_chain_tips()? .into_iter() .filter_map(|t| { @@ -47,17 +47,17 @@ pub fn analysis(mut rpc: AutoReconnectRPC, mut db: AutoReconnectDb, state: &Wire .max() .unwrap_or(0) as u16; // The first time we see a fork that big - if max_fork > max_seen { - max_seen = max_fork; + if fork > max_seen { + max_seen = fork; let current_conf = state.confirmation.load(Ordering::SeqCst); // If new fork is bigger than the current confirmation - if max_fork > current_conf { + if fork > current_conf { // Max two time the configuration - let new_conf = max_fork.min(state.config.confirmation * 2); + let new_conf = fork.min(state.config.confirmation * 2); state.confirmation.store(new_conf, Ordering::SeqCst); warn!( "analysis: found dangerous fork of {} blocks, adapt confirmation to {} blocks capped at {}, you should update taler.conf", - max_fork, new_conf, state.config.confirmation * 2 + fork, new_conf, state.config.confirmation * 2 ); } } diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs @@ -22,7 +22,7 @@ use btc_wire::{ use common::{ config::{load_btc_config, BtcConfig}, log::log::info, - reconnect::auto_reconnect_db, + reconnect::auto_reconnect_db, named_spawn, }; use std::{sync::atomic::AtomicU16, thread::JoinHandle}; @@ -88,15 +88,3 @@ fn main() { }); worker(rpc_worker, db_worker, state); } - -pub fn named_spawn<F, T>(name: impl Into<String>, f: F) -> JoinHandle<T> -where - F: FnOnce() -> T, - F: Send + 'static, - T: Send + 'static, -{ - std::thread::Builder::new() - .name(name.into()) - .spawn(f) - .unwrap() -} diff --git a/common/src/lib.rs b/common/src/lib.rs @@ -1,3 +1,5 @@ +use std::thread::JoinHandle; + /* This file is part of TALER Copyright (C) 2022 Taler Systems SA @@ -34,3 +36,16 @@ pub fn rand_slice<const N: usize>() -> [u8; N] { OsRng.fill_bytes(slice.as_mut_slice()); slice } + +/// Spawned a named thread +pub fn named_spawn<F, T>(name: impl Into<String>, f: F) -> JoinHandle<T> +where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, +{ + std::thread::Builder::new() + .name(name.into()) + .spawn(f) + .unwrap() +} diff --git a/eth-wire/src/loops.rs b/eth-wire/src/loops.rs @@ -16,3 +16,4 @@ pub mod watcher; pub mod worker; +pub mod analysis; diff --git a/eth-wire/src/loops/analysis.rs b/eth-wire/src/loops/analysis.rs @@ -0,0 +1,97 @@ +/* + 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::sync::atomic::Ordering; + +use common::{ + log::log::{error, warn}, + postgres::fallible_iterator::FallibleIterator, + reconnect::AutoReconnectDb, +}; +use eth_wire::rpc::{AutoReconnectRPC, Rpc}; +use ethereum_types::{H256, U64}; + +use crate::{LoopResult, WireState}; + +/// Analyse blockchain behavior and adapt confirmations in real time +pub fn analysis(mut rpc: AutoReconnectRPC, mut db: AutoReconnectDb, state: &WireState) { + // The biggest fork ever seen + let mut max_seen = 0; + let mut tip: Option<(U64, H256)> = None; + loop { + let rpc = rpc.client(); + let db = db.client(); + let result: LoopResult<()> = (|| { + // Register as listener + db.batch_execute("LISTEN new_block")?; + loop { + // Get biggest known valid fork + let (fork, new_tip) = check_fork(rpc, &tip)?; + tip.replace(new_tip); + // The first time we see a fork that big + if fork > max_seen { + max_seen = fork; + let current_conf = state.confirmation.load(Ordering::SeqCst); + // If new fork is bigger than the current confirmation + if fork > current_conf { + // Max two time the configuration + let new_conf = fork.min(state.config.confirmation * 2); + state.confirmation.store(new_conf, Ordering::SeqCst); + warn!( + "analysis: found dangerous fork of {} blocks, adapt confirmation to {} blocks capped at {}, you should update taler.conf", + fork, new_conf, state.config.confirmation * 2 + ); + } + } + + // TODO smarter analysis: suspicious transaction value, limit wire bitcoin throughput + + // Wait for next notification + db.notifications().blocking_iter().next()?; + } + })(); + if let Err(e) = result { + error!("analysis: {}", e); + } + } +} + +/// Return fork size and new tip +pub fn check_fork(rpc: &mut Rpc, tip: &Option<(U64, H256)>) -> LoopResult<(u16, (U64, H256))> { + let mut size = 0; + let latest = rpc.latest_block()?; + if let Some((number, hash)) = tip { + let mut chain_cursor = latest.clone(); + // Skip until tip height + while chain_cursor.number.unwrap() != *number { + chain_cursor = rpc.block(&chain_cursor.parent_hash)?.unwrap(); + size += 1; + } + + // Check fork + if chain_cursor.hash.unwrap() != *hash { + let mut fork_cursor = rpc.block(hash)?.unwrap(); + while fork_cursor.hash != chain_cursor.hash { + chain_cursor = rpc.block(&chain_cursor.parent_hash)?.unwrap(); + fork_cursor = rpc.block(&fork_cursor.parent_hash)?.unwrap(); + size += 1; + } + } else { + size = 0; + } + } + Ok((size, (latest.number.unwrap(), latest.hash.unwrap()))) +} diff --git a/eth-wire/src/main.rs b/eth-wire/src/main.rs @@ -18,7 +18,7 @@ use std::sync::atomic::AtomicU16; use common::{ config::{load_eth_config, EthConfig}, - postgres, + named_spawn, postgres, reconnect::auto_reconnect_db, }; use eth_wire::{ @@ -27,7 +27,7 @@ use eth_wire::{ }; use ethereum_types::H160; use fail_point::Injected; -use loops::{watcher::watcher, worker::worker}; +use loops::{analysis::analysis, watcher::watcher, worker::worker}; mod fail_point; mod loops; @@ -66,13 +66,19 @@ fn main() { })); let rpc_worker = auto_reconnect_rpc(state.config.core.data_dir.clone().unwrap(), state.address); + let rpc_analysis = + auto_reconnect_rpc(state.config.core.data_dir.clone().unwrap(), state.address); let rpc_watcher = auto_reconnect_rpc(state.config.core.data_dir.clone().unwrap(), state.address); let db_watcher = auto_reconnect_db(state.config.core.db_url.clone()); + let db_analysis = auto_reconnect_db(state.config.core.db_url.clone()); let db_worker = auto_reconnect_db(state.config.core.db_url.clone()); - std::thread::spawn(move || watcher(rpc_watcher, db_watcher)); + named_spawn("watcher", move || watcher(rpc_watcher, db_watcher)); + named_spawn("analysis", move || { + analysis(rpc_analysis, db_analysis, state) + }); worker(rpc_worker, db_worker, state); } diff --git a/makefile b/makefile @@ -25,5 +25,7 @@ test_eth: install test/eth/reconnect.sh test/eth/stress.sh test/eth/reorg.sh + test/eth/hell.sh + test/eth/analysis.sh test: install test_gateway test_eth test_btc \ No newline at end of file diff --git a/test/eth/analysis.sh b/test/eth/analysis.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +## Test eth-wire ability to learn and protect itself from blockchain behavior + +set -eu + +source "${BASH_SOURCE%/*}/../common.sh" +SCHEMA=eth.sql +CONFIG=taler_eth.conf + +echo "----- Setup -----" +echo "Load config file" +load_config +echo "Start database" +setup_db +echo "Start ethereum node" +init_eth +echo "Start second ethereum node" +init_eth2 +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 deposit $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 5 +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 deposit $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 5 +check_balance_eth 999580000 420000 +gateway_up +echo " OK" + +echo "All tests passed!" +\ No newline at end of file