diff --git a/common/request/src/lib.rs b/common/request/src/lib.rs index ad452a0c..60e51019 100644 --- a/common/request/src/lib.rs +++ b/common/request/src/lib.rs @@ -55,6 +55,8 @@ impl Client { fn connector() -> Connector { let mut res = HttpConnector::new(); res.set_keepalive(Some(core::time::Duration::from_secs(60))); + res.set_nodelay(true); + res.set_reuse_address(true); #[cfg(feature = "tls")] let res = HttpsConnectorBuilder::new() .with_native_roots() @@ -68,7 +70,9 @@ impl Client { pub fn with_connection_pool() -> Client { Client { connection: Connection::ConnectionPool( - HyperClient::builder(TokioExecutor::new()).build(Self::connector()), + HyperClient::builder(TokioExecutor::new()) + .pool_idle_timeout(core::time::Duration::from_secs(60)) + .build(Self::connector()), ), } } diff --git a/orchestration/dev/coins/ethereum/run.sh b/orchestration/dev/coins/ethereum/run.sh index 4fee3e46..464f4c6e 100755 --- a/orchestration/dev/coins/ethereum/run.sh +++ b/orchestration/dev/coins/ethereum/run.sh @@ -1,3 +1,3 @@ #!/bin/sh -~/.foundry/bin/anvil --no-mining --slots-in-an-epoch 32 +~/.foundry/bin/anvil --host 0.0.0.0 --no-cors --no-mining --slots-in-an-epoch 32 --silent diff --git a/processor/Cargo.toml b/processor/Cargo.toml index cbc022a1..f90f6117 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -84,7 +84,7 @@ serai-docker-tests = { path = "../tests/docker" } secp256k1 = ["k256", "frost/secp256k1"] bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin"] -ethereum = ["secp256k1", "ethereum-serai"] +ethereum = ["secp256k1", "ethereum-serai/tests"] ed25519 = ["dalek-ff-group", "frost/ed25519"] monero = ["ed25519", "monero-serai", "serai-client/monero"] diff --git a/processor/src/networks/ethereum.rs b/processor/src/networks/ethereum.rs index 4de08837..3bb012ca 100644 --- a/processor/src/networks/ethereum.rs +++ b/processor/src/networks/ethereum.rs @@ -124,7 +124,7 @@ impl SignableTransaction for RouterCommand { } #[async_trait] -impl TransactionTrait> for Transaction { +impl TransactionTrait> for Transaction { type Id = [u8; 32]; fn id(&self) -> Self::Id { self.hash.0 @@ -157,7 +157,7 @@ impl Epoch { } #[async_trait] -impl Block> for Epoch { +impl Block> for Epoch { type Id = [u8; 32]; fn id(&self) -> [u8; 32] { self.end_hash @@ -170,7 +170,7 @@ impl Block> for Epoch { } } -impl Output> for EthereumInInstruction { +impl Output> for EthereumInInstruction { type Id = [u8; 32]; fn kind(&self) -> OutputType { @@ -282,8 +282,8 @@ impl EventualityTrait for Eventuality { } } -#[derive(Clone, Debug)] -pub struct Ethereum { +#[derive(Clone)] +pub struct Ethereum { // This DB is solely used to access the first key generated, as needed to determine the Router's // address. Accordingly, all methods present are consistent to a Serai chain with a finalized // first key (regardless of local state), and this is safe. @@ -292,20 +292,26 @@ pub struct Ethereum { deployer: Deployer, router: Arc>>, } -impl PartialEq for Ethereum { +impl PartialEq for Ethereum { fn eq(&self, _other: &Ethereum) -> bool { true } } -impl Ethereum { +impl fmt::Debug for Ethereum { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("Ethereum") + .field("deployer", &self.deployer) + .field("router", &self.router) + .finish_non_exhaustive() + } +} +impl Ethereum { pub async fn new(db: D, url: String) -> Self { let provider = Arc::new(RootProvider::new( ClientBuilder::default().transport(SimpleRequest::new(url), true), )); - #[cfg(test)] // TODO: Move to test code - provider.raw_request::<_, ()>("evm_setAutomine".into(), false).await.unwrap(); - let mut deployer = Deployer::new(provider.clone()).await; while !matches!(deployer, Ok(Some(_))) { log::error!("Deployer wasn't deployed yet or networking error"); @@ -362,7 +368,7 @@ impl Ethereum { } #[async_trait] -impl Network for Ethereum { +impl Network for Ethereum { type Curve = Secp256k1; type Transaction = Transaction; @@ -479,7 +485,8 @@ impl Network for Ethereum { // Grab the key at the end of the epoch let key_at_end_of_block = loop { match router.key_at_end_of_block(block.start + 31).await { - Ok(key) => break key, + Ok(Some(key)) => break key, + Ok(None) => return vec![], Err(e) => { log::error!("couldn't connect to router for the key at the end of the block: {e:?}"); sleep(Duration::from_secs(5)).await; @@ -491,17 +498,7 @@ impl Network for Ethereum { let mut all_events = vec![]; let mut top_level_txids = HashSet::new(); for erc20_addr in [DAI] { - let erc20 = loop { - let Ok(Some(erc20)) = Erc20::new(self.provider.clone(), erc20_addr).await else { - log::error!( - "couldn't connect to Ethereum node for an ERC20: {}", - hex::encode(erc20_addr) - ); - sleep(Duration::from_secs(5)).await; - continue; - }; - break erc20; - }; + let erc20 = Erc20::new(self.provider.clone(), erc20_addr); for block in block.start .. (block.start + 32) { let transfers = loop { @@ -821,6 +818,7 @@ impl Network for Ethereum { .provider .get_transaction_by_hash(log.clone().transaction_hash.unwrap()) .await + .unwrap() .unwrap(); }; @@ -830,20 +828,26 @@ impl Network for Ethereum { .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() + self + .provider + .get_transaction_by_hash(logs[0].transaction_hash.unwrap()) + .await + .unwrap() + .unwrap() } } } #[cfg(test)] async fn mine_block(&self) { - self.provider.raw_request::<_, ()>("anvil_mine".into(), [32]).await.unwrap(); + self.provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); } #[cfg(test)] async fn test_send(&self, send_to: Self::Address) -> Self::Block { use rand_core::OsRng; use ciphersuite::group::ff::Field; + use ethereum_serai::alloy_sol_types::SolCall; let key = ::F::random(&mut OsRng); let address = ethereum_serai::crypto::address(&(Secp256k1::generator() * key)); @@ -858,15 +862,22 @@ impl Network for Ethereum { .await .unwrap(); + let value = U256::from_str_radix("1000000000000000000", 10).unwrap(); let tx = ethereum_serai::alloy_consensus::TxLegacy { chain_id: None, nonce: 0, - gas_price: 100_000_000_000u128, - gas_limit: 21_0000u128, + gas_price: 1_000_000_000u128, + gas_limit: 200_000u128, to: ethereum_serai::alloy_core::primitives::TxKind::Call(send_to.0.into()), // 1 ETH - value: U256::from_str_radix("1000000000000000000", 10).unwrap(), - input: vec![].into(), + value, + input: ethereum_serai::router::abi::inInstructionCall::new(( + [0; 20].into(), + value, + vec![].into(), + )) + .abi_encode() + .into(), }; use ethereum_serai::alloy_consensus::SignableTransaction; diff --git a/processor/src/tests/addresses.rs b/processor/src/tests/addresses.rs index 8f730dbd..3d4d6d4c 100644 --- a/processor/src/tests/addresses.rs +++ b/processor/src/tests/addresses.rs @@ -1,4 +1,4 @@ -use core::time::Duration; +use core::{time::Duration, pin::Pin, future::Future}; use std::collections::HashMap; use rand_core::OsRng; @@ -82,8 +82,9 @@ async fn spend( } } -pub async fn test_addresses(network: N) -where +pub async fn test_addresses( + new_network: impl Fn(MemDb) -> Pin>>, +) where >::Addendum: From<()>, { let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng); @@ -92,12 +93,14 @@ where } let key = keys[&Participant::new(1).unwrap()].group_key(); + let mut db = MemDb::new(); + let network = new_network(db.clone()).await; + // Mine blocks so there's a confirmed block for _ in 0 .. N::CONFIRMATIONS { network.mine_block().await; } - let mut db = MemDb::new(); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); assert!(current_keys.is_empty()); let mut txn = db.txn(); diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index e2bfdc8a..20aa1083 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -3,6 +3,8 @@ use dockertest::{ TestBodySpecification, DockerOperations, DockerTest, }; +use serai_db::MemDb; + #[cfg(feature = "bitcoin")] mod bitcoin { use std::sync::Arc; @@ -33,8 +35,6 @@ mod bitcoin { sync::Mutex, }; - use serai_db::MemDb; - use super::*; use crate::{ networks::{Network, Bitcoin, Output, OutputType, Block}, @@ -57,7 +57,7 @@ mod bitcoin { fn test_receive_data_from_input() { let docker = spawn_bitcoin(); docker.run(|ops| async move { - let btc = bitcoin(&ops).await; + let btc = bitcoin(&ops).await(MemDb::new()).await; // generate a multisig address to receive the coins let mut keys = frost::tests::key_gen::<_, ::Curve>(&mut OsRng) @@ -208,23 +208,26 @@ mod bitcoin { test } - async fn bitcoin(ops: &DockerOperations) -> Bitcoin { + async fn bitcoin( + ops: &DockerOperations, + ) -> impl Fn(MemDb) -> Pin>> { let handle = ops.handle("serai-dev-bitcoin").host_port(8332).unwrap(); - let bitcoin = Bitcoin::new(format!("http://serai:seraidex@{}:{}", handle.0, handle.1)).await; + let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1); + let bitcoin = Bitcoin::new(url.clone()).await; bitcoin.fresh_chain().await; - bitcoin + move |_db| Box::pin(Bitcoin::new(url.clone())) } - test_network!( + test_utxo_network!( Bitcoin, spawn_bitcoin, bitcoin, bitcoin_key_gen, bitcoin_scanner, + bitcoin_no_deadlock_in_multisig_completed, bitcoin_signer, bitcoin_wallet, bitcoin_addresses, - bitcoin_no_deadlock_in_multisig_completed, ); } @@ -252,24 +255,181 @@ mod monero { test } - async fn monero(ops: &DockerOperations) -> Monero { + async fn monero( + ops: &DockerOperations, + ) -> impl Fn(MemDb) -> Pin>> { let handle = ops.handle("serai-dev-monero").host_port(18081).unwrap(); - let monero = Monero::new(format!("http://serai:seraidex@{}:{}", handle.0, handle.1)).await; + let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1); + let monero = Monero::new(url.clone()).await; while monero.get_latest_block_number().await.unwrap() < 150 { monero.mine_block().await; } - monero + move |_db| Box::pin(Monero::new(url.clone())) } - test_network!( + test_utxo_network!( Monero, spawn_monero, monero, monero_key_gen, monero_scanner, + monero_no_deadlock_in_multisig_completed, monero_signer, monero_wallet, monero_addresses, - monero_no_deadlock_in_multisig_completed, + ); +} + +#[cfg(feature = "ethereum")] +mod ethereum { + use super::*; + + use ciphersuite::{Ciphersuite, Secp256k1}; + + use serai_client::validator_sets::primitives::Session; + + use crate::networks::Ethereum; + + fn spawn_ethereum() -> DockerTest { + serai_docker_tests::build("ethereum".to_string()); + + let composition = TestBodySpecification::with_image( + Image::with_repository("serai-dev-ethereum").pull_policy(PullPolicy::Never), + ) + .set_start_policy(StartPolicy::Strict) + .set_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::OnError, + source: LogSource::Both, + })) + .set_publish_all_ports(true); + + let mut test = DockerTest::new(); + test.provide_container(composition); + test + } + + async fn ethereum( + ops: &DockerOperations, + ) -> impl Fn(MemDb) -> Pin>>> { + use std::sync::Arc; + use ethereum_serai::{ + alloy_core::primitives::U256, + alloy_simple_request_transport::SimpleRequest, + alloy_rpc_client::ClientBuilder, + alloy_provider::{Provider, RootProvider}, + deployer::Deployer, + }; + + let handle = ops.handle("serai-dev-ethereum").host_port(8545).unwrap(); + let url = format!("http://{}:{}", handle.0, handle.1); + tokio::time::sleep(core::time::Duration::from_secs(15)).await; + + { + let provider = Arc::new(RootProvider::new( + ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true), + )); + provider.raw_request::<_, ()>("evm_setAutomine".into(), [false]).await.unwrap(); + provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); + + // Perform deployment + { + // Make sure the Deployer constructor returns None, as it doesn't exist yet + assert!(Deployer::new(provider.clone()).await.unwrap().is_none()); + + // Deploy the Deployer + let tx = Deployer::deployment_tx(); + + 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 (tx, sig, _) = tx.into_parts(); + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig, &mut bytes); + + let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap(); + provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); + //tokio::time::sleep(core::time::Duration::from_secs(15)).await; + let receipt = pending_tx.get_receipt().await.unwrap(); + assert!(receipt.status()); + + let _ = Deployer::new(provider.clone()) + .await + .expect("network error") + .expect("deployer wasn't deployed"); + } + } + + move |db| { + let url = url.clone(); + Box::pin(async move { + { + let db = db.clone(); + let url = url.clone(); + // Spawn a task to deploy the proper Router when the time comes + tokio::spawn(async move { + let key = loop { + let Some(key) = crate::key_gen::NetworkKeyDb::get(&db, Session(0)) else { + tokio::time::sleep(core::time::Duration::from_secs(1)).await; + continue; + }; + break ethereum_serai::crypto::PublicKey::new( + Secp256k1::read_G(&mut key.as_slice()).unwrap(), + ) + .unwrap(); + }; + let provider = Arc::new(RootProvider::new( + ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true), + )); + let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap(); + + let mut tx = deployer.deploy_router(&key); + tx.gas_limit = 1_000_000u64.into(); + tx.gas_price = 1_000_000_000u64.into(); + let tx = ethereum_serai::crypto::deterministically_sign(&tx); + + 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 (tx, sig, _) = tx.into_parts(); + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig, &mut bytes); + let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap(); + provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); + let receipt = pending_tx.get_receipt().await.unwrap(); + assert!(receipt.status()); + + let _router = deployer.find_router(provider.clone(), &key).await.unwrap().unwrap(); + }); + } + + Ethereum::new(db, url.clone()).await + }) + } + } + + test_network!( + Ethereum, + spawn_ethereum, + ethereum, + ethereum_key_gen, + ethereum_scanner, + ethereum_no_deadlock_in_multisig_completed, ); } diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 974be10b..26b49635 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,22 +1,18 @@ use std::sync::OnceLock; mod key_gen; -pub(crate) use key_gen::test_key_gen; mod scanner; -pub(crate) use scanner::{test_scanner, test_no_deadlock_in_multisig_completed}; mod signer; -pub(crate) use signer::{sign, test_signer}; +pub(crate) use signer::sign; mod cosigner; mod batch_signer; mod wallet; -pub(crate) use wallet::test_wallet; mod addresses; -pub(crate) use addresses::test_addresses; // Effective Once static INIT_LOGGER_CELL: OnceLock<()> = OnceLock::new(); @@ -27,22 +23,21 @@ fn init_logger() { #[macro_export] macro_rules! test_network { ( - $N: ident, + $N: ty, $docker: ident, $network: ident, $key_gen: ident, $scanner: ident, - $signer: ident, - $wallet: ident, - $addresses: ident, $no_deadlock_in_multisig_completed: ident, ) => { + use core::{pin::Pin, future::Future}; use $crate::tests::{ - init_logger, test_key_gen, test_scanner, test_no_deadlock_in_multisig_completed, test_signer, - test_wallet, test_addresses, + init_logger, + key_gen::test_key_gen, + scanner::{test_scanner, test_no_deadlock_in_multisig_completed}, }; - // This doesn't interact with a node and accordingly doesn't need to be run + // This doesn't interact with a node and accordingly doesn't need to be spawn one #[tokio::test] async fn $key_gen() { init_logger(); @@ -54,34 +49,8 @@ macro_rules! test_network { init_logger(); let docker = $docker(); docker.run(|ops| async move { - test_scanner($network(&ops).await).await; - }); - } - - #[test] - fn $signer() { - init_logger(); - let docker = $docker(); - docker.run(|ops| async move { - test_signer($network(&ops).await).await; - }); - } - - #[test] - fn $wallet() { - init_logger(); - let docker = $docker(); - docker.run(|ops| async move { - test_wallet($network(&ops).await).await; - }); - } - - #[test] - fn $addresses() { - init_logger(); - let docker = $docker(); - docker.run(|ops| async move { - test_addresses($network(&ops).await).await; + let new_network = $network(&ops).await; + test_scanner(new_network).await; }); } @@ -90,7 +59,57 @@ macro_rules! test_network { init_logger(); let docker = $docker(); docker.run(|ops| async move { - test_no_deadlock_in_multisig_completed($network(&ops).await).await; + let new_network = $network(&ops).await; + 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() { + init_logger(); + let docker = $docker(); + docker.run(|ops| async move { + let new_network = $network(&ops).await; + test_signer(new_network).await; + }); + } + + #[test] + fn $wallet() { + init_logger(); + let docker = $docker(); + docker.run(|ops| async move { + let new_network = $network(&ops).await; + test_wallet(new_network).await; + }); + } + + #[test] + fn $addresses() { + init_logger(); + let docker = $docker(); + docker.run(|ops| async move { + let new_network = $network(&ops).await; + test_addresses(new_network).await; }); } }; diff --git a/processor/src/tests/scanner.rs b/processor/src/tests/scanner.rs index 42756d8b..16885dab 100644 --- a/processor/src/tests/scanner.rs +++ b/processor/src/tests/scanner.rs @@ -1,21 +1,23 @@ -use core::time::Duration; +use core::{pin::Pin, time::Duration, future::Future}; use std::sync::Arc; -use ciphersuite::Ciphersuite; use rand_core::OsRng; +use ciphersuite::{group::GroupEncoding, Ciphersuite}; use frost::{Participant, tests::key_gen}; use tokio::{sync::Mutex, time::timeout}; use serai_db::{DbTxn, Db, MemDb}; +use serai_client::validator_sets::primitives::Session; use crate::{ - networks::{OutputType, Output, Block, UtxoNetwork}, + networks::{OutputType, Output, Block, Network}, + key_gen::NetworkKeyDb, multisigs::scanner::{ScannerEvent, Scanner, ScannerHandle}, }; -pub async fn new_scanner( +pub async fn new_scanner( network: &N, db: &D, group_key: ::G, @@ -40,18 +42,27 @@ pub async fn new_scanner( scanner } -pub async fn test_scanner(network: N) { +pub async fn test_scanner( + new_network: impl Fn(MemDb) -> Pin>>, +) { let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap(); N::tweak_keys(&mut keys); let group_key = keys.group_key(); + let mut db = MemDb::new(); + { + let mut txn = db.txn(); + NetworkKeyDb::set(&mut txn, Session(0), &group_key.to_bytes().as_ref().to_vec()); + txn.commit(); + } + let network = new_network(db.clone()).await; + // Mine blocks so there's a confirmed block for _ in 0 .. N::CONFIRMATIONS { network.mine_block().await; } - let db = MemDb::new(); let first = Arc::new(Mutex::new(true)); let scanner = new_scanner(&network, &db, group_key, &first).await; @@ -101,13 +112,17 @@ pub async fn test_scanner(network: N) { .is_err()); } -pub async fn test_no_deadlock_in_multisig_completed(network: N) { +pub async fn test_no_deadlock_in_multisig_completed( + new_network: impl Fn(MemDb) -> Pin>>, +) { + let mut db = MemDb::new(); + let network = new_network(db.clone()).await; + // Mine blocks so there's a confirmed block for _ in 0 .. N::CONFIRMATIONS { network.mine_block().await; } - let mut db = MemDb::new(); let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); assert!(current_keys.is_empty()); diff --git a/processor/src/tests/signer.rs b/processor/src/tests/signer.rs index 524c5d29..85444d63 100644 --- a/processor/src/tests/signer.rs +++ b/processor/src/tests/signer.rs @@ -1,3 +1,4 @@ +use core::{pin::Pin, future::Future}; use std::collections::HashMap; use rand_core::{RngCore, OsRng}; @@ -153,8 +154,9 @@ pub async fn sign( typed_claim } -pub async fn test_signer(network: N) -where +pub async fn test_signer( + new_network: impl Fn(MemDb) -> Pin>>, +) where >::Addendum: From<()>, { let mut keys = key_gen(&mut OsRng); @@ -163,6 +165,9 @@ where } let key = keys[&Participant::new(1).unwrap()].group_key(); + let db = MemDb::new(); + let network = new_network(db).await; + let outputs = network .get_outputs(&network.test_send(N::external_address(&network, key).await).await, key) .await; diff --git a/processor/src/tests/wallet.rs b/processor/src/tests/wallet.rs index 4600fcbe..acd3cb65 100644 --- a/processor/src/tests/wallet.rs +++ b/processor/src/tests/wallet.rs @@ -1,4 +1,5 @@ -use std::{time::Duration, collections::HashMap}; +use core::{time::Duration, pin::Pin, future::Future}; +use std::collections::HashMap; use rand_core::OsRng; @@ -24,12 +25,9 @@ use crate::{ }; // Tests the Scanner, Scheduler, and Signer together -pub async fn test_wallet(network: N) { - // Mine blocks so there's a confirmed block - for _ in 0 .. N::CONFIRMATIONS { - network.mine_block().await; - } - +pub async fn test_wallet( + new_network: impl Fn(MemDb) -> Pin>>, +) { let mut keys = key_gen(&mut OsRng); for keys in keys.values_mut() { N::tweak_keys(keys); @@ -37,6 +35,13 @@ pub async fn test_wallet(network: N) { let key = keys[&Participant::new(1).unwrap()].group_key(); let mut db = MemDb::new(); + let network = new_network(db.clone()).await; + + // Mine blocks so there's a confirmed block + for _ in 0 .. N::CONFIRMATIONS { + network.mine_block().await; + } + let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone()); assert!(current_keys.is_empty()); let (block_id, outputs) = {