diff --git a/Cargo.lock b/Cargo.lock index 332d693f97..7aad51b9c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,6 +465,21 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1632,6 +1647,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "librocksdb-sys" version = "0.11.0+8.1.1" @@ -1965,6 +1986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm 0.2.7", ] [[package]] @@ -2075,7 +2097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" dependencies = [ "cfg-if", - "libm", + "libm 0.1.4", ] [[package]] @@ -2254,6 +2276,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +dependencies = [ + "bit-set", + "bitflags 1.3.2", + "byteorder", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.6.29", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "quanta" version = "0.11.1" @@ -2270,6 +2312,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.23.1" @@ -2563,6 +2611,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rusty-hook" version = "0.11.2" @@ -3023,6 +3083,7 @@ dependencies = [ "itertools", "open", "parking_lot", + "proptest", "rand", "rand_chacha", "serde", @@ -3031,6 +3092,7 @@ dependencies = [ "snarkos-node-messages", "snarkos-node-tcp", "snarkvm", + "test-strategy", "time", "tokio", "tokio-stream", @@ -3846,6 +3908,29 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "structmeta" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" +dependencies = [ + "proc-macro2", + "quote 1.0.29", + "structmeta-derive", + "syn 2.0.23", +] + +[[package]] +name = "structmeta-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" +dependencies = [ + "proc-macro2", + "quote 1.0.29", + "syn 2.0.23", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3914,6 +3999,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "test-strategy" +version = "0.3.0" +source = "git+https://github.com/frozenlib/test-strategy/?branch=master#885013160c1431970a80faa43a5c96a9e05385ce" +dependencies = [ + "proc-macro2", + "quote 1.0.29", + "structmeta", + "syn 2.0.23", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -4313,6 +4409,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.6.0" @@ -4442,6 +4544,15 @@ version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.3" diff --git a/node/narwhal/Cargo.toml b/node/narwhal/Cargo.toml index c9eabdd72c..a364224219 100644 --- a/node/narwhal/Cargo.toml +++ b/node/narwhal/Cargo.toml @@ -113,3 +113,7 @@ features = [ "fs", "trace" ] [dev-dependencies.tracing-subscriber] version = "0.3" features = [ "env-filter" ] + +[dev-dependencies] +test-strategy = { git = "https://github.com/frozenlib/test-strategy/", branch = "master"} +proptest = "1.0.0" \ No newline at end of file diff --git a/node/narwhal/src/gateway.rs b/node/narwhal/src/gateway.rs index 9fafabfd5e..308b503c85 100644 --- a/node/narwhal/src/gateway.rs +++ b/node/narwhal/src/gateway.rs @@ -76,6 +76,7 @@ impl Gateway { pub fn new(committee: Arc>>, account: Account, dev: Option) -> Result { // Initialize the gateway IP. let ip = match dev { + // TODO change dev to Option, otherwise there is potential overflow Some(dev) => SocketAddr::from_str(&format!("127.0.0.1:{}", MEMORY_POOL_PORT + dev)), None => SocketAddr::from_str(&format!("0.0.0.0:{}", MEMORY_POOL_PORT)), }?; @@ -793,3 +794,124 @@ impl Gateway { None } } + +#[cfg(test)] +pub mod gateway_tests { + use crate::{ + helpers::{ + committee_tests::{CommitteeInput, Validator}, + init_worker_channels, + storage_tests::StorageInput, + WorkerSender, + }, + Gateway, + Worker, + MAX_COMMITTEE_SIZE, + MAX_WORKERS, + MEMORY_POOL_PORT, + }; + use indexmap::IndexMap; + use parking_lot::RwLock; + use snarkos_node_tcp::P2P; + use snarkvm::prelude::Testnet3; + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, + }; + use test_strategy::{proptest, Arbitrary}; + + type N = Testnet3; + + #[derive(Arbitrary, Debug, Clone)] + pub struct GatewayInput { + #[filter(CommitteeInput::is_valid)] + pub committee_input: CommitteeInput, + pub node_validator: Validator, + pub dev: Option, + #[strategy(0..MAX_WORKERS)] + pub workers_count: u8, + pub worker_storage: StorageInput, + } + + impl GatewayInput { + pub fn to_gateway(&self) -> Gateway { + let committee = self.committee_input.to_committee().unwrap(); + let account = self.node_validator.get_account(); + let dev = self.dev.map(|dev| dev as u16); + Gateway::new(Arc::new(RwLock::new(committee)), account, dev).unwrap() + } + + pub async fn generate_workers( + &self, + gateway: &Gateway, + ) -> (IndexMap>, IndexMap>) { + // Construct a map of the worker senders. + let mut tx_workers = IndexMap::new(); + let mut workers = IndexMap::new(); + + // Initialize the workers. + for id in 0..self.workers_count { + // Construct the worker channels. + let (tx_worker, rx_worker) = init_worker_channels(); + // Construct the worker instance. + let mut worker = Worker::new(id, gateway.clone(), self.worker_storage.to_storage()).unwrap(); + // Run the worker instance. + worker.run(rx_worker).await.unwrap(); + + // Add the worker and the worker sender to maps + workers.insert(id, worker); + tx_workers.insert(id, tx_worker); + } + (workers, tx_workers) + } + } + + #[proptest] + fn gateway_initialization(input: GatewayInput) { + let account = input.node_validator.get_account(); + let address = account.address(); + + let gateway = match input.dev { + Some(dev) => { + let gateway = input.to_gateway(); + let tcp_config = gateway.tcp().config(); + assert_eq!(tcp_config.listener_ip, Some(IpAddr::V4(Ipv4Addr::LOCALHOST))); + assert_eq!(tcp_config.desired_listening_port, Some(MEMORY_POOL_PORT + (dev as u16))); + gateway + } + None => { + let gateway = input.to_gateway(); + let tcp_config = gateway.tcp().config(); + assert_eq!(tcp_config.listener_ip, Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED))); + assert_eq!(tcp_config.desired_listening_port, Some(MEMORY_POOL_PORT)); + gateway + } + }; + let tcp_config = gateway.tcp().config(); + assert_eq!(tcp_config.max_connections, MAX_COMMITTEE_SIZE); + assert_eq!(gateway.account().address(), address); + } + + #[proptest(async = "tokio")] + async fn gateway_start(#[filter(|x| x.dev.is_some())] input: GatewayInput) { + let Some(dev) = input.dev else { unreachable!() }; + let mut gateway = input.to_gateway(); + let tcp_config = gateway.tcp().config(); + assert_eq!(tcp_config.listener_ip, Some(IpAddr::V4(Ipv4Addr::LOCALHOST))); + assert_eq!(tcp_config.desired_listening_port, Some(MEMORY_POOL_PORT + (dev as u16))); + + let (workers, worker_senders) = input.generate_workers(&gateway).await; + match gateway.run(worker_senders).await { + Ok(_) => { + assert_eq!( + gateway.local_ip(), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), MEMORY_POOL_PORT + (dev as u16)) + ); + assert_eq!(gateway.num_workers(), workers.len() as u8); + } + Err(err) => { + unreachable!("Unexpected {err}"); + } + } + } +} diff --git a/node/narwhal/src/helpers/committee.rs b/node/narwhal/src/helpers/committee.rs index 280385980c..a98228055b 100644 --- a/node/narwhal/src/helpers/committee.rs +++ b/node/narwhal/src/helpers/committee.rs @@ -113,3 +113,105 @@ impl Committee { Ok(power) } } + +#[cfg(test)] +pub mod committee_tests { + use crate::helpers::Committee; + use anyhow::Result; + use indexmap::IndexMap; + use proptest::sample::size_range; + use rand::SeedableRng; + use snarkos_account::Account; + use snarkvm::prelude::Testnet3; + use test_strategy::{proptest, Arbitrary}; + + type N = Testnet3; + + #[derive(Arbitrary, Debug, Clone)] + pub struct CommitteeInput { + #[strategy(0u64..)] + pub round: u64, + #[any(size_range(0..32).lift())] + pub validators: Vec, + } + + #[derive(Arbitrary, Debug, Clone)] + pub struct Validator { + #[strategy(..5_000_000_000u64)] + pub stake: u64, + account_seed: u64, + } + + impl Validator { + pub fn get_account(&self) -> Account { + match Account::new(&mut rand_chacha::ChaChaRng::seed_from_u64(self.account_seed)) { + Ok(account) => account, + Err(err) => panic!("Failed to create account {err}"), + } + } + } + + impl CommitteeInput { + pub fn to_committee(&self) -> Result> { + let mut index_map = IndexMap::new(); + for validator in self.validators.iter() { + index_map.insert(validator.get_account().address(), validator.stake); + } + Committee::new(self.round, index_map) + } + + pub fn is_valid(&self) -> bool { + self.round > 0 && self.validators.len() >= 4 + } + } + + #[proptest] + fn committee_advance(#[filter(CommitteeInput::is_valid)] input: CommitteeInput) { + let committee = input.to_committee().unwrap(); + let current_round = input.round; + let current_members = committee.members(); + assert_eq!(committee.round(), current_round); + + let committee = committee.to_next_round().unwrap(); + assert_eq!(committee.round(), current_round + 1); + assert_eq!(committee.members(), current_members); + } + + #[proptest] + fn committee_members(input: CommitteeInput) { + let committee = match input.to_committee() { + Ok(committee) => { + assert!(input.is_valid()); + committee + } + Err(err) => { + assert!(!input.is_valid()); + match err.to_string().as_str() { + "Round must be nonzero" => assert_eq!(input.round, 0), + "Committee must have at least 4 members" => assert!(input.validators.len() < 4), + _ => panic!("Unexpected error: {err}"), + } + return Ok(()); + } + }; + + let validators = input.validators; + + let mut total_stake = 0; + for v in validators.iter() { + total_stake += v.stake; + } + + assert_eq!(committee.committee_size(), validators.len()); + assert_eq!(committee.total_stake().unwrap(), total_stake); + for v in validators.iter() { + let address = v.get_account().address(); + assert!(committee.is_committee_member(address)); + assert_eq!(committee.get_stake(address), v.stake); + } + let quorum_threshold = committee.quorum_threshold().unwrap(); + let availability_threshold = committee.availability_threshold().unwrap(); + // (2f + 1) + (f + 1) - 1 = 3f + 1 = N + assert_eq!(quorum_threshold + availability_threshold - 1, total_stake); + } +} diff --git a/node/narwhal/src/helpers/storage.rs b/node/narwhal/src/helpers/storage.rs index 6384a43d65..f6f9f3482c 100644 --- a/node/narwhal/src/helpers/storage.rs +++ b/node/narwhal/src/helpers/storage.rs @@ -412,7 +412,7 @@ impl Storage { } #[cfg(test)] -mod tests { +pub mod storage_tests { use super::*; use snarkvm::{ ledger::narwhal::Data, @@ -422,6 +422,7 @@ mod tests { use ::bytes::Bytes; use indexmap::indexset; + use test_strategy::Arbitrary; type CurrentNetwork = snarkvm::prelude::Testnet3; @@ -566,4 +567,15 @@ mod tests { // Ensure the certificate is no longer stored in the round. assert!(storage.get_certificates_for_round(round).is_empty()); } + + #[derive(Arbitrary, Debug, Clone)] + pub struct StorageInput { + pub gc_rounds: u64, + } + + impl StorageInput { + pub fn to_storage(&self) -> Storage { + Storage::new(self.gc_rounds) + } + } } diff --git a/node/narwhal/src/worker.rs b/node/narwhal/src/worker.rs index 561d41c38a..6e507543fc 100644 --- a/node/narwhal/src/worker.rs +++ b/node/narwhal/src/worker.rs @@ -267,3 +267,15 @@ impl Worker { self.handles.lock().iter().for_each(|handle| handle.abort()); } } + +#[cfg(test)] +mod worker_tests { + use crate::helpers::storage_tests::StorageInput; + use test_strategy::Arbitrary; + + #[derive(Arbitrary, Debug, Clone)] + pub struct WorkerInput { + pub id: u8, + pub storage: StorageInput, + } +}