commit f8e660fa3915e3b7f4320dda808db582d20e6de6
parent 60e4b9be19afea1a91e0b29cf4ff14af6f41dbfa
Author: Antoine A <>
Date: Thu, 10 Feb 2022 15:40:15 +0100
eth-wire: add analysis
Diffstat:
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