/* 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 */ use std::{ ops::{Deref, DerefMut}, path::Path, 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::{H160, H256, U256}; use crate::utils::{ check_incoming, check_outgoing, cmd_out, cmd_redirect, cmd_redirect_ok, print_now, retry, retry_opt, transfer, unused_port, ChildGuard, TalerCtx, TestCtx, }; fn wait_for_pending(rpc: &mut Rpc) { print_now("Wait for pending transactions mining:"); let mut rpc = rpc.subscribe_new_head().unwrap(); std::thread::sleep(Duration::from_secs(1)); // Wait for eth-wire to act while !rpc.pending_transactions().unwrap().is_empty() { rpc.next().unwrap(); print_now("."); std::thread::sleep(Duration::from_secs(1)); // Wait for eth-wire to act } println!(); } pub fn online_test(config: Option<&Path>, base_url: &str) { let state = WireState::load_taler_config(config); // TODO eth network check let min_fund = U256::from(100_000 * TRUNC); let test_amount = U256::from(20_000 * TRUNC); let taler_test_amount = eth_to_taler(&test_amount, state.currency); let mut rpc = Rpc::new(state.ipc_path).unwrap(); // Load client let client_addr = rpc .list_accounts() .unwrap() .into_iter() .skip(1) // Skip etherbase if dev network .find(|addr| addr != &state.address) // Skip wire .unwrap_or_else(|| rpc.new_account("password").unwrap()); // Else create account rpc.unlock_account(&client_addr, "password").unwrap(); 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(), hex::encode(client_addr) ); print_now("Waiting for fund:"); let mut rpc = rpc.subscribe_new_head().unwrap(); while rpc.get_balance_latest(&client_addr).unwrap() < min_fund { rpc.next().unwrap(); print_now("."); } println!(); } wait_for_pending(&mut rpc); // Load balances 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 { tip_hash: latest.hash.unwrap(), tip_height: latest.number.unwrap(), conf_height: latest.number.unwrap(), }; println!("Send transaction"); let reserve_pub_key = rand_slice(); let credit_id = rpc .credit(client_addr, state.address, test_amount, reserve_pub_key) .unwrap(); let zero_id = rpc .send_transaction(&TransactionRequest { from: client_addr, to: state.address, value: U256::zero(), gas_price: None, data: Hex(vec![]), nonce: None, }) .unwrap(); let bounce_id = rpc .send_transaction(&TransactionRequest { from: client_addr, to: state.address, value: test_amount, gas_price: None, data: Hex(vec![]), nonce: None, }) .unwrap(); print_now("Wait for bounce:"); let bounce = { let mut rpc = rpc.subscribe_new_head().unwrap(); 'l: loop { let list = rpc.list_since_sync(&state.address, sync_state, 0).unwrap(); sync_state = list.state; for sync_tx in list.txs { let tx = sync_tx.tx; if tx.to.unwrap() == client_addr && tx.from.unwrap() == state.address { let metadata = OutMetadata::decode(&tx.input).unwrap(); match metadata { OutMetadata::Debit { .. } => {} OutMetadata::Bounce { bounced } => { let bounced = H256::from_slice(&bounced); if bounced == bounce_id { break 'l tx; } else if bounced == zero_id { panic!("Bounced zero"); } } } } } rpc.next().unwrap(); print_now("."); } }; println!(); wait_for_pending(&mut rpc); println!("Check balance"); 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() .map(|id| { let receipt = rpc.get_transaction_receipt(&id).unwrap().unwrap(); receipt.gas_used * receipt.effective_gas_price.unwrap() }) .reduce(|acc, i| acc + i) .unwrap(); assert_eq!( client_balance - client_sent_amount_cost - client_sent_fees_cost + bounce.value, new_client_balance ); let receipt = rpc.get_transaction_receipt(&bounce.hash).unwrap().unwrap(); let bounced_fee = receipt.gas_used * receipt.effective_gas_price.unwrap(); assert_eq!( wire_balance + test_amount + (test_amount - bounce.value - bounced_fee), new_wire_balance ); 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, &wtid, &state.base_url, eth_payto_url(&client_addr), &taler_test_amount, ); wait_for_pending(&mut rpc); println!("Check balances"); let last_client_balance = rpc.get_balance_latest(&client_addr).unwrap(); assert_eq!(new_client_balance + test_amount, last_client_balance); 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, ctx: TalerCtx, passwd: String, } impl Deref for EthCtx { type Target = TalerCtx; fn deref(&self) -> &Self::Target { &self.ctx } } impl DerefMut for EthCtx { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.ctx } } impl EthCtx { pub fn setup(ctx: &TestCtx, config: &str, stressed: bool) -> Self { let mut ctx = TalerCtx::new(ctx, "eth-wire", config, stressed); // Init chain let passwd = std::env::var("PASSWORD").unwrap(); let pswd_path = ctx.dir.path().join("pswd"); std::fs::write(&pswd_path, passwd.as_bytes()).unwrap(); for _ in ["reserve", "client"] { cmd_redirect_ok( "geth", &[ "--datadir", ctx.wire_dir.to_str().unwrap(), "--lightkdf", "account", "new", "--password", pswd_path.to_str().unwrap(), ], &ctx.log("geth"), "create account", ) } let list = cmd_out( "geth", &[ "--datadir", ctx.wire_dir.to_str().unwrap(), "--lightkdf", "account", "list", ], ); let reserve = &list[13..][..40]; let client = &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, \"clique\": {{ \"period\": 1 }} }}, \"difficulty\": \"1\", \"gasLimit\": \"0\", \"baseFeePerGas\": null, \"extraData\": \"0x0000000000000000000000000000000000000000000000000000000000000000{}0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\", \"alloc\": {{ \"{}\": {{ \"balance\": \"10000000000000000000\" }} }} }}", reserve, client ); std::fs::write(ctx.wire_dir.join("genesis.json"), genesis.as_bytes()).unwrap(); cmd_redirect_ok( "geth", &[ "--datadir", ctx.wire_dir.to_str().unwrap(), "--lightkdf", "init", ctx.wire_dir.join("genesis.json").to_str().unwrap(), ], &ctx.log("geth"), "init chain", ); cmd_redirect_ok( "geth", &[ "--datadir", ctx.wire2_dir.to_str().unwrap(), "--lightkdf", "init", ctx.wire_dir.join("genesis.json").to_str().unwrap(), ], &ctx.log("geth2"), "init chain2", ); let node = cmd_redirect( "geth", &[ "--datadir", ctx.wire_dir.to_str().unwrap(), "--lightkdf", "--miner.gasprice", "10", "--authrpc.port", &unused_port().to_string(), "--port", &unused_port().to_string(), "--rpc.enabledeprecatedpersonal", ], &ctx.log("geth"), ); let mut rpc = retry_opt(|| Rpc::new(&ctx.wire_dir).ok()); ctx.init_db(); // Generate wallet let out = cmd_out( &ctx.wire_bin_path, &["-c", ctx.conf.to_str().unwrap(), "initwallet"], ); let mut config = ini::Ini::load_from_file(&ctx.conf).unwrap(); config.with_section(Some("depolymerizer-ethereum")).set( "PAYTO", out.lines().nth(6).unwrap().split(" = ").last().unwrap(), ); config.write_to_file(&ctx.conf).unwrap(); ctx.run(); let state = WireState::load_taler_config(Some(&ctx.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(); } Self { node, rpc, reserve_addr, client_addr, wire_addr, conf: state.confirmation as u16, state, ctx, passwd, } } pub fn reset_db(&mut self) { let block = self.rpc.earliest_block().unwrap(); let mut db = self.ctx.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_slice()], ) .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.ctx.dir.path().join("chain"); let path = path.to_str().unwrap(); Self::export(&mut self.rpc, path); cmd_redirect_ok( "geth", &[ "--datadir", self.ctx.wire2_dir.to_str().unwrap(), "--keystore", self.ctx.wire_dir.join("keystore").to_str().unwrap(), "--lightkdf", "import", path, ], &self.ctx.log("geth2"), "import chain", ); } pub fn cluster_fork(&mut self, length: u16) { let node2 = cmd_redirect( "geth", &[ "--datadir", self.ctx.wire2_dir.to_str().unwrap(), "--keystore", self.ctx.wire_dir.join("keystore").to_str().unwrap(), "--lightkdf", "--miner.gasprice", "10", "--authrpc.port", &unused_port().to_string(), "--port", &unused_port().to_string(), "--rpc.enabledeprecatedpersonal", ], &self.ctx.log("geth2"), ); let mut rpc = retry_opt(|| Rpc::new(&self.ctx.wire2_dir).ok()); Self::_mine(&mut rpc, &self.reserve_addr, length, &self.passwd); let path = self.ctx.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.ctx.wire_dir.to_str().unwrap(), "--lightkdf", "import", path, ], &self.ctx.log("geth"), "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 (pa, pb) = (unused_port().to_string(), unused_port().to_string()); let mut args = vec![ "--datadir", self.ctx.wire_dir.to_str().unwrap(), "--lightkdf", "--authrpc.port", &pa, "--port", &pb, "--rpc.enabledeprecatedpersonal", ]; args.extend_from_slice(additional_args); self.node = cmd_redirect("geth", &args, &self.ctx.log("geth")); self.rpc = retry_opt(|| Rpc::new(&self.ctx.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 { 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.ctx.gateway_url, &metadata, &self.state.base_url, eth_payto_url(&self.client_addr), ð_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(); rpc.miner_set_etherbase(addr).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.ctx.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.ctx.expect_debits(&self.state.base_url, &txs) } } /// Test eth-wire correctly receive and send transactions on the blockchain pub fn wire(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth.conf", false); ctx.step("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); }; ctx.step("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); } ctx.step("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 pub fn lifetime(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth_lifetime.conf", false); ctx.step("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 pub fn reconnect(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth.conf", false); let mut credits = Vec::new(); let mut debits = Vec::new(); ctx.step("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); }; ctx.step("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(); } ctx.step("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); } ctx.step("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 pub fn stress(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth.conf", true); let mut credits = Vec::new(); let mut debits = Vec::new(); ctx.step("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); }; ctx.step("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); } ctx.step("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); } ctx.step("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 pub fn reorg(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth.conf", false); ctx.step("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(); } ctx.step("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); } ctx.step("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 pub fn hell(ctx: TestCtx) { fn step(ctx: &TestCtx, name: &str, action: impl FnOnce(&mut EthCtx)) { ctx.step("Setup"); let mut ctx = EthCtx::setup(ctx, "taler_eth.conf", false); ctx.step(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(&ctx, "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(&ctx, "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 pub fn analysis(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "taler_eth.conf", false); ctx.step("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(5); 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); std::thread::sleep(Duration::from_secs(3)); ctx.expect_gateway_up(); } /// Test eth-wire ability to handle stuck transaction correctly pub fn bumpfee(tctx: TestCtx) { tctx.step("Setup"); let mut ctx = EthCtx::setup(&tctx, "taler_eth_bump.conf", false); // Perform credits to allow wire to perform debits latter ctx.credit(ctx.amount(90000000), rand_slice()); ctx.next_conf(); ctx.step("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); } ctx.step("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); } ctx.step("Setup"); drop(ctx); let mut ctx = EthCtx::setup(&tctx, "taler_eth_bump.conf", true); // Perform credit to allow wire to perform debits latter ctx.credit(ctx.amount(9000000), rand_slice()); ctx.next_conf(); ctx.step("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 pub fn maxfee(ctx: TestCtx) { ctx.step("Setup"); let mut ctx = EthCtx::setup(&ctx, "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(); ctx.step("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); } ctx.step("Good feed"); { // Restore default config ctx.restart_node(&[""]); // Check transaction now have been made ctx.expect_client_balance(client + total_amount, true); } }