From d27d93480aa8a849d84214ad4c71d83ce6fea0c1 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 11 May 2024 00:11:14 -0400 Subject: [PATCH] Get processor signer/wallet tests working for Ethereum They are handicapped by the fact Ethereum self-sends don't show up as outputs, yet that's fundamental (unless we add a *harmful* fallback function). --- deny.toml | 2 +- .../src/multisigs/scheduler/smart_contract.rs | 4 +- processor/src/networks/ethereum.rs | 110 +++++++++--------- processor/src/networks/mod.rs | 7 +- processor/src/tests/literal/mod.rs | 2 + processor/src/tests/mod.rs | 51 +++++--- processor/src/tests/signer.rs | 96 ++++++++------- processor/src/tests/wallet.rs | 74 ++++++++---- 8 files changed, 204 insertions(+), 142 deletions(-) diff --git a/deny.toml b/deny.toml index 603312899..d6972d5e9 100644 --- a/deny.toml +++ b/deny.toml @@ -101,5 +101,5 @@ allow-git = [ "https://github.com/serai-dex/substrate", "https://github.com/alloy-rs/alloy", "https://github.com/monero-rs/base58-monero", - "https://github.com/kayabaNerve/dockertest-rs", + "https://github.com/orcalabs/dockertest-rs", ] diff --git a/processor/src/multisigs/scheduler/smart_contract.rs b/processor/src/multisigs/scheduler/smart_contract.rs index 27268b822..4f48e391f 100644 --- a/processor/src/multisigs/scheduler/smart_contract.rs +++ b/processor/src/multisigs/scheduler/smart_contract.rs @@ -116,7 +116,7 @@ impl> SchedulerTrait for Scheduler { assert!(self.coins.contains(&utxo.balance().coin)); } - let mut nonce = LastNonce::get(txn).map_or(0, |nonce| nonce + 1); + let mut nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1); let mut plans = vec![]; for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) { // Once we rotate, all further payments should be scheduled via the new multisig @@ -179,7 +179,7 @@ impl> SchedulerTrait for Scheduler { .and_then(|key_bytes| ::read_G(&mut key_bytes.as_slice()).ok()) .unwrap_or(self.key); - let nonce = LastNonce::get(txn).map_or(0, |nonce| nonce + 1); + let nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1); LastNonce::set(txn, &(nonce + 1)); Plan { key: current_key, diff --git a/processor/src/networks/ethereum.rs b/processor/src/networks/ethereum.rs index 3bb012caf..f3d562d7c 100644 --- a/processor/src/networks/ethereum.rs +++ b/processor/src/networks/ethereum.rs @@ -719,22 +719,6 @@ impl Network for Ethereum { // Publish this using a dummy account we fund with magic RPC commands #[cfg(test)] { - use rand_core::OsRng; - use ciphersuite::group::ff::Field; - - let key = ::F::random(&mut OsRng); - let address = ethereum_serai::crypto::address(&(Secp256k1::generator() * key)); - - // Set a 1.1 ETH balance - self - .provider - .raw_request::<_, ()>( - "anvil_setBalance".into(), - [Address(address).to_string(), "1100000000000000000".into()], - ) - .await - .unwrap(); - let router = self.router().await; let router = router.as_ref().unwrap(); @@ -747,17 +731,30 @@ impl Network for Ethereum { completion.signature(), ), }; - tx.gas_price = 100_000_000_000u128; + tx.gas_limit = 1_000_000u64.into(); + tx.gas_price = 1_000_000_000u64.into(); + let tx = ethereum_serai::crypto::deterministically_sign(&tx); - use ethereum_serai::alloy_consensus::SignableTransaction; - let sig = - k256::ecdsa::SigningKey::from(k256::elliptic_curve::NonZeroScalar::new(key).unwrap()) - .sign_prehash_recoverable(tx.signature_hash().as_ref()) + if self.provider.get_transaction_by_hash(*tx.hash()).await.unwrap().is_none() { + self + .provider + .raw_request::<_, ()>( + "anvil_setBalance".into(), + [ + tx.recover_signer().unwrap().to_string(), + (U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(), + ], + ) + .await .unwrap(); - let mut bytes = vec![]; - tx.encode_with_signature_fields(&sig.into(), &mut bytes); - let _ = self.provider.send_raw_transaction(&bytes).await.ok().unwrap(); + let (tx, sig, _) = tx.into_parts(); + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig, &mut bytes); + let pending_tx = self.provider.send_raw_transaction(&bytes).await.unwrap(); + self.mine_block().await; + assert!(pending_tx.get_receipt().await.unwrap().status()); + } Ok(()) } @@ -801,41 +798,50 @@ impl Network for Ethereum { block: usize, eventuality: &Self::Eventuality, ) -> Self::Transaction { - match eventuality.1 { - RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { - let router = self.router().await; - let router = router.as_ref().unwrap(); - - let block = u64::try_from(block).unwrap(); - let filter = router - .key_updated_filter() - .from_block(block * 32) - .to_block(((block + 1) * 32) - 1) - .topic1(nonce); - let logs = self.provider.get_logs(&filter).await.unwrap(); - if let Some(log) = logs.first() { + // We mine 96 blocks to ensure the 32 blocks relevant are finalized + // Back-check the prior two epochs in response to this + // TODO: Review why this is sub(3) and not sub(2) + for block in block.saturating_sub(3) ..= block { + match eventuality.1 { + RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { + let router = self.router().await; + let router = router.as_ref().unwrap(); + + let block = u64::try_from(block).unwrap(); + let filter = router + .key_updated_filter() + .from_block(block * 32) + .to_block(((block + 1) * 32) - 1) + .topic1(nonce); + let logs = self.provider.get_logs(&filter).await.unwrap(); + if let Some(log) = logs.first() { + return self + .provider + .get_transaction_by_hash(log.clone().transaction_hash.unwrap()) + .await + .unwrap() + .unwrap(); + }; + + let filter = router + .executed_filter() + .from_block(block * 32) + .to_block(((block + 1) * 32) - 1) + .topic1(nonce); + let logs = self.provider.get_logs(&filter).await.unwrap(); + if logs.is_empty() { + continue; + } return self .provider - .get_transaction_by_hash(log.clone().transaction_hash.unwrap()) + .get_transaction_by_hash(logs[0].transaction_hash.unwrap()) .await .unwrap() .unwrap(); - }; - - let filter = router - .executed_filter() - .from_block(block * 32) - .to_block(((block + 1) * 32) - 1) - .topic1(nonce); - let logs = self.provider.get_logs(&filter).await.unwrap(); - self - .provider - .get_transaction_by_hash(logs[0].transaction_hash.unwrap()) - .await - .unwrap() - .unwrap() + } } } + panic!("couldn't find completion in any three of checked blocks"); } #[cfg(test)] diff --git a/processor/src/networks/mod.rs b/processor/src/networks/mod.rs index 803ed40ab..ee3cd24af 100644 --- a/processor/src/networks/mod.rs +++ b/processor/src/networks/mod.rs @@ -432,9 +432,12 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Debug { let plan_id = plan.id(); let Plan { key, inputs, mut payments, change, scheduler_addendum } = plan; - let theoretical_change_amount = + let theoretical_change_amount = if change.is_some() { inputs.iter().map(|input| input.balance().amount.0).sum::() - - payments.iter().map(|payment| payment.balance.amount.0).sum::(); + payments.iter().map(|payment| payment.balance.amount.0).sum::() + } else { + 0 + }; let Some(tx_fee) = self.needed_fee(block_number, &inputs, &payments, &change).await? else { // This Plan is not fulfillable diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 20aa10832..cecd5a3bf 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -431,5 +431,7 @@ mod ethereum { ethereum_key_gen, ethereum_scanner, ethereum_no_deadlock_in_multisig_completed, + ethereum_signer, + ethereum_wallet, ); } diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 26b496350..7ab57bdef 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -29,12 +29,16 @@ macro_rules! test_network { $key_gen: ident, $scanner: ident, $no_deadlock_in_multisig_completed: ident, + $signer: ident, + $wallet: ident, ) => { use core::{pin::Pin, future::Future}; use $crate::tests::{ init_logger, key_gen::test_key_gen, scanner::{test_scanner, test_no_deadlock_in_multisig_completed}, + signer::test_signer, + wallet::test_wallet, }; // This doesn't interact with a node and accordingly doesn't need to be spawn one @@ -63,25 +67,6 @@ macro_rules! test_network { test_no_deadlock_in_multisig_completed(new_network).await; }); } - }; -} - -#[macro_export] -macro_rules! test_utxo_network { - ( - $N: ty, - $docker: ident, - $network: ident, - $key_gen: ident, - $scanner: ident, - $no_deadlock_in_multisig_completed: ident, - $signer: ident, - $wallet: ident, - $addresses: ident, - ) => { - use $crate::tests::{signer::test_signer, wallet::test_wallet, addresses::test_addresses}; - - test_network!($N, $docker, $network, $key_gen, $scanner, $no_deadlock_in_multisig_completed,); #[test] fn $signer() { @@ -102,6 +87,34 @@ macro_rules! test_utxo_network { test_wallet(new_network).await; }); } + }; +} + +#[macro_export] +macro_rules! test_utxo_network { + ( + $N: ty, + $docker: ident, + $network: ident, + $key_gen: ident, + $scanner: ident, + $no_deadlock_in_multisig_completed: ident, + $signer: ident, + $wallet: ident, + $addresses: ident, + ) => { + use $crate::tests::addresses::test_addresses; + + test_network!( + $N, + $docker, + $network, + $key_gen, + $scanner, + $no_deadlock_in_multisig_completed, + $signer, + $wallet, + ); #[test] fn $addresses() { diff --git a/processor/src/tests/signer.rs b/processor/src/tests/signer.rs index 85444d63e..77307ef26 100644 --- a/processor/src/tests/signer.rs +++ b/processor/src/tests/signer.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use rand_core::{RngCore, OsRng}; +use ciphersuite::group::GroupEncoding; use frost::{ Participant, ThresholdKeys, dkg::tests::{key_gen, clone_without}, @@ -17,14 +18,15 @@ use serai_client::{ use messages::sign::*; use crate::{ - Payment, Plan, - networks::{Output, Transaction, Eventuality, UtxoNetwork}, + Payment, + networks::{Output, Transaction, Eventuality, Network}, + key_gen::NetworkKeyDb, multisigs::scheduler::Scheduler, signer::Signer, }; #[allow(clippy::type_complexity)] -pub async fn sign( +pub async fn sign( network: N, session: Session, mut keys_txs: HashMap< @@ -154,57 +156,55 @@ pub async fn sign( typed_claim } -pub async fn test_signer( +pub async fn test_signer( new_network: impl Fn(MemDb) -> Pin>>, -) where - >::Addendum: From<()>, -{ +) { let mut keys = key_gen(&mut OsRng); for keys in keys.values_mut() { N::tweak_keys(keys); } let key = keys[&Participant::new(1).unwrap()].group_key(); - let db = MemDb::new(); - let network = new_network(db).await; + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec()); + txn.commit(); + } + let network = new_network(db.clone()).await; let outputs = network .get_outputs(&network.test_send(N::external_address(&network, key).await).await, key) .await; let sync_block = network.get_latest_block_number().await.unwrap() - N::CONFIRMATIONS; - let amount = 2 * N::DUST; + let amount = (2 * N::DUST) + 1000; + let plan = { + let mut txn = db.txn(); + let mut scheduler = N::Scheduler::new::(&mut txn, key, N::NETWORK); + let payments = vec![Payment { + address: N::external_address(&network, key).await, + data: None, + balance: Balance { + coin: match N::NETWORK { + NetworkId::Serai => panic!("test_signer called with Serai"), + NetworkId::Bitcoin => Coin::Bitcoin, + NetworkId::Ethereum => Coin::Ether, + NetworkId::Monero => Coin::Monero, + }, + amount: Amount(amount), + }, + }]; + let mut plans = scheduler.schedule::(&mut txn, outputs.clone(), payments, key, false); + assert_eq!(plans.len(), 1); + plans.swap_remove(0) + }; + let mut keys_txs = HashMap::new(); let mut eventualities = vec![]; for (i, keys) in keys.drain() { - let (signable, eventuality) = network - .prepare_send( - sync_block, - Plan { - key, - inputs: outputs.clone(), - payments: vec![Payment { - address: N::external_address(&network, key).await, - data: None, - balance: Balance { - coin: match N::NETWORK { - NetworkId::Serai => panic!("test_signer called with Serai"), - NetworkId::Bitcoin => Coin::Bitcoin, - NetworkId::Ethereum => Coin::Ether, - NetworkId::Monero => Coin::Monero, - }, - amount: Amount(amount), - }, - }], - change: Some(N::change_address(key).unwrap()), - scheduler_addendum: ().into(), - }, - 0, - ) - .await - .unwrap() - .tx - .unwrap(); + let (signable, eventuality) = + network.prepare_send(sync_block, plan.clone(), 0).await.unwrap().tx.unwrap(); eventualities.push(eventuality.clone()); keys_txs.insert(i, (keys, (signable, eventuality))); @@ -222,11 +222,21 @@ pub async fn test_signer( key, ) .await; - assert_eq!(outputs.len(), 2); - // Adjust the amount for the fees - let amount = amount - tx.fee(&network).await; - // Check either output since Monero will randomize its output order - assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)); + // Don't run if Ethereum as the received output will revert by the contract + // (and therefore not actually exist) + if N::NETWORK != NetworkId::Ethereum { + assert_eq!(outputs.len(), 1 + usize::from(u8::from(plan.change.is_some()))); + // Adjust the amount for the fees + let amount = amount - tx.fee(&network).await; + if plan.change.is_some() { + // Check either output since Monero will randomize its output order + assert!( + (outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount) + ); + } else { + assert!(outputs[0].balance().amount.0 == amount); + } + } // Check the eventualities pass for eventuality in eventualities { diff --git a/processor/src/tests/wallet.rs b/processor/src/tests/wallet.rs index acd3cb650..86a27349d 100644 --- a/processor/src/tests/wallet.rs +++ b/processor/src/tests/wallet.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use rand_core::OsRng; +use ciphersuite::group::GroupEncoding; use frost::{Participant, dkg::tests::key_gen}; use tokio::time::timeout; @@ -16,16 +17,17 @@ use serai_client::{ use crate::{ Payment, Plan, - networks::{Output, Transaction, Eventuality, Block, UtxoNetwork}, + networks::{Output, Transaction, Eventuality, Block, Network}, + key_gen::NetworkKeyDb, multisigs::{ scanner::{ScannerEvent, Scanner}, - scheduler::Scheduler, + scheduler::{self, Scheduler}, }, tests::sign, }; // Tests the Scanner, Scheduler, and Signer together -pub async fn test_wallet( +pub async fn test_wallet( new_network: impl Fn(MemDb) -> Pin>>, ) { let mut keys = key_gen(&mut OsRng); @@ -35,6 +37,11 @@ pub async fn test_wallet( let key = keys[&Participant::new(1).unwrap()].group_key(); let mut db = MemDb::new(); + { + let mut txn = db.txn(); + NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec()); + txn.commit(); + } let network = new_network(db.clone()).await; // Mine blocks so there's a confirmed block @@ -98,7 +105,13 @@ pub async fn test_wallet( txn.commit(); assert_eq!(plans.len(), 1); assert_eq!(plans[0].key, key); - assert_eq!(plans[0].inputs, outputs); + if std::any::TypeId::of::() == + std::any::TypeId::of::>() + { + assert_eq!(plans[0].inputs, vec![]); + } else { + assert_eq!(plans[0].inputs, outputs); + } assert_eq!( plans[0].payments, vec![Payment { @@ -115,7 +128,7 @@ pub async fn test_wallet( } }] ); - assert_eq!(plans[0].change, Some(N::change_address(key).unwrap())); + assert_eq!(plans[0].change, N::change_address(key)); { let mut buf = vec![]; @@ -144,9 +157,22 @@ pub async fn test_wallet( let tx = network.get_transaction_by_eventuality(block_number, &eventualities[0]).await; let block = network.get_block(block_number).await.unwrap(); let outputs = network.get_outputs(&block, key).await; - assert_eq!(outputs.len(), 2); - let amount = amount - tx.fee(&network).await; - assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)); + + // Don't run if Ethereum as the received output will revert by the contract + // (and therefore not actually exist) + if N::NETWORK != NetworkId::Ethereum { + assert_eq!(outputs.len(), 1 + usize::from(u8::from(plans[0].change.is_some()))); + // Adjust the amount for the fees + let amount = amount - tx.fee(&network).await; + if plans[0].change.is_some() { + // Check either output since Monero will randomize its output order + assert!( + (outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount) + ); + } else { + assert!(outputs[0].balance().amount.0 == amount); + } + } for eventuality in eventualities { let completion = network.confirm_completion(&eventuality, &claim).await.unwrap().unwrap(); @@ -157,21 +183,23 @@ pub async fn test_wallet( network.mine_block().await; } - match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { - ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => { - scanner.multisig_completed.send(false).unwrap(); - assert!(!is_retirement_block); - assert_eq!(block_id, block.id()); - assert_eq!(these_outputs, outputs); - } - ScannerEvent::Completed(_, _, _, _, _) => { - panic!("unexpectedly got eventuality completion"); + if N::NETWORK != NetworkId::Ethereum { + match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { + ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => { + scanner.multisig_completed.send(false).unwrap(); + assert!(!is_retirement_block); + assert_eq!(block_id, block.id()); + assert_eq!(these_outputs, outputs); + } + ScannerEvent::Completed(_, _, _, _, _) => { + panic!("unexpectedly got eventuality completion"); + } } - } - // Check the Scanner DB can reload the outputs - let mut txn = db.txn(); - assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs); - scanner.release_lock().await; - txn.commit(); + // Check the Scanner DB can reload the outputs + let mut txn = db.txn(); + assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs); + scanner.release_lock().await; + txn.commit(); + } }