diff --git a/Cargo.toml b/Cargo.toml index 1a1d0f8a..96b13d0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,6 @@ members = [ "nomos-services/mempool", "nomos-services/http", "nodes/nomos-node", - "nodes/mockpool-node" + "nodes/mockpool-node", + "simulations" ] diff --git a/simulations/Cargo.toml b/simulations/Cargo.toml new file mode 100644 index 00000000..7b67530a --- /dev/null +++ b/simulations/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "simulations" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = { version = "0.8", features = ["small_rng"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + diff --git a/simulations/src/bin/app.rs b/simulations/src/bin/app.rs new file mode 100644 index 00000000..8df0541b --- /dev/null +++ b/simulations/src/bin/app.rs @@ -0,0 +1,3 @@ +pub fn main() { + println!("WIP"); +} diff --git a/simulations/src/lib.rs b/simulations/src/lib.rs new file mode 100644 index 00000000..bde0e784 --- /dev/null +++ b/simulations/src/lib.rs @@ -0,0 +1,4 @@ +pub mod network; +pub mod node; +pub mod overlay; +pub mod runner; diff --git a/simulations/src/network/behaviour.rs b/simulations/src/network/behaviour.rs new file mode 100644 index 00000000..cae6784a --- /dev/null +++ b/simulations/src/network/behaviour.rs @@ -0,0 +1,26 @@ +// std +use std::time::Duration; +// crates +use rand::Rng; +use serde::{Deserialize, Serialize}; +// internal + +#[derive(Default, Serialize, Deserialize)] +pub struct NetworkBehaviour { + pub delay: Duration, + pub drop: f64, +} + +impl NetworkBehaviour { + pub fn new(delay: Duration, drop: f64) -> Self { + Self { delay, drop } + } + + pub fn delay(&self) -> Duration { + self.delay + } + + pub fn should_drop(&self, rng: &mut R) -> bool { + rng.gen_bool(self.drop) + } +} diff --git a/simulations/src/network/mod.rs b/simulations/src/network/mod.rs new file mode 100644 index 00000000..fc781f9b --- /dev/null +++ b/simulations/src/network/mod.rs @@ -0,0 +1,31 @@ +// std +use std::time::Duration; +// crates +use rand::Rng; +// internal +use crate::node::NodeId; + +pub mod behaviour; +pub mod regions; + +pub struct Network { + pub regions: regions::RegionsData, +} + +impl Network { + pub fn new(regions: regions::RegionsData) -> Self { + Self { regions } + } + + pub fn send_message_cost( + &self, + rng: &mut R, + node_a: NodeId, + node_b: NodeId, + ) -> Option { + let network_behaviour = self.regions.network_behaviour(node_a, node_b); + (!network_behaviour.should_drop(rng)) + // TODO: use a delay range + .then(|| network_behaviour.delay()) + } +} diff --git a/simulations/src/network/regions.rs b/simulations/src/network/regions.rs new file mode 100644 index 00000000..83301f80 --- /dev/null +++ b/simulations/src/network/regions.rs @@ -0,0 +1,58 @@ +// std +use std::collections::HashMap; +// crates +use serde::{Deserialize, Serialize}; +// internal +use crate::{network::behaviour::NetworkBehaviour, node::NodeId}; + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum Region { + NorthAmerica, + Europe, + Asia, + Africa, + SouthAmerica, + Australia, +} + +#[derive(Serialize, Deserialize)] +pub struct RegionsData { + pub regions: HashMap>, + #[serde(skip)] + pub node_region: HashMap, + pub region_network_behaviour: HashMap<(Region, Region), NetworkBehaviour>, +} + +impl RegionsData { + pub fn new( + regions: HashMap>, + region_network_behaviour: HashMap<(Region, Region), NetworkBehaviour>, + ) -> Self { + let node_region = regions + .iter() + .flat_map(|(region, nodes)| nodes.iter().copied().map(|node| (node, *region))) + .collect(); + Self { + regions, + node_region, + region_network_behaviour, + } + } + + pub fn node_region(&self, node_id: NodeId) -> Region { + self.node_region[&node_id] + } + + pub fn network_behaviour(&self, node_a: NodeId, node_b: NodeId) -> &NetworkBehaviour { + let region_a = self.node_region[&node_a]; + let region_b = self.node_region[&node_b]; + self.region_network_behaviour + .get(&(region_a, region_b)) + .or(self.region_network_behaviour.get(&(region_b, region_a))) + .expect("Network behaviour not found for the given regions") + } + + pub fn region_nodes(&self, region: Region) -> &[NodeId] { + &self.regions[®ion] + } +} diff --git a/simulations/src/node/carnot.rs b/simulations/src/node/carnot.rs new file mode 100644 index 00000000..233bbb55 --- /dev/null +++ b/simulations/src/node/carnot.rs @@ -0,0 +1,156 @@ +// std +use std::collections::HashMap; +use std::hash::Hash; +use std::rc::Rc; +// crates +use rand::prelude::SmallRng; +use rand::{Rng, SeedableRng}; +// internal +use crate::network::Network; +use crate::node::{Node, NodeId, StepTime}; +use crate::overlay::{Committee, Layout}; + +pub type ParentCommitteeReceiverSolver = + fn(&mut SmallRng, NodeId, &Committee, &Network) -> StepTime; + +pub type ChildCommitteeReceiverSolver = + fn(&mut SmallRng, NodeId, &[&Committee], &Network) -> StepTime; + +fn receive_proposal( + rng: &mut SmallRng, + node: NodeId, + committee: &Committee, + network: &Network, +) -> StepTime { + assert!(!committee.is_empty()); + committee + .iter() + .filter_map(|&sender| network.send_message_cost(rng, sender, node)) + .max() + .unwrap() +} + +fn receive_commit( + rng: &mut SmallRng, + node: NodeId, + committees: &[&Committee], + network: &Network, +) -> StepTime { + assert!(!committees.is_empty()); + committees + .iter() + .filter_map(|committee| { + committee + .iter() + .filter_map(|&sender| network.send_message_cost(rng, sender, node)) + .max() + }) + .max() + .unwrap() +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum CarnotStep { + ReceiveProposal, + ValidateProposal, + ReceiveVote, + ValidateVote, +} + +#[derive(Clone)] +pub enum CarnotStepSolver { + Plain(StepTime), + ParentCommitteeReceiverSolver(ParentCommitteeReceiverSolver), + ChildCommitteeReceiverSolver(ChildCommitteeReceiverSolver), +} + +pub struct CarnotNodeSettings { + pub steps_costs: HashMap, + pub network: Network, + pub layout: Layout, +} + +pub struct CarnotNode { + id: NodeId, + rng: SmallRng, + settings: Rc, +} + +pub const CARNOT_STEPS_COSTS: &[(CarnotStep, CarnotStepSolver)] = &[ + ( + CarnotStep::ReceiveProposal, + CarnotStepSolver::ParentCommitteeReceiverSolver(receive_proposal), + ), + ( + CarnotStep::ValidateProposal, + CarnotStepSolver::Plain(StepTime::from_secs(1)), + ), + ( + CarnotStep::ReceiveVote, + CarnotStepSolver::ChildCommitteeReceiverSolver(receive_commit), + ), + ( + CarnotStep::ValidateVote, + CarnotStepSolver::Plain(StepTime::from_secs(1)), + ), +]; + +pub const CARNOT_LEADER_STEPS: &[CarnotStep] = &[CarnotStep::ReceiveVote, CarnotStep::ValidateVote]; + +pub const CARNOT_ROOT_STEPS: &[CarnotStep] = &[ + CarnotStep::ReceiveProposal, + CarnotStep::ValidateProposal, + CarnotStep::ReceiveVote, + CarnotStep::ValidateVote, +]; + +pub const CARNOT_INTERMEDIATE_STEPS: &[CarnotStep] = &[ + CarnotStep::ReceiveProposal, + CarnotStep::ValidateProposal, + CarnotStep::ReceiveVote, + CarnotStep::ValidateVote, +]; + +pub const CARNOT_LEAF_STEPS: &[CarnotStep] = + &[CarnotStep::ReceiveProposal, CarnotStep::ValidateProposal]; + +impl Node for CarnotNode { + type Settings = Rc; + type Step = CarnotStep; + + fn new(rng: &mut R, id: NodeId, settings: Self::Settings) -> Self { + let rng = SmallRng::from_rng(rng).unwrap(); + Self { id, settings, rng } + } + + fn id(&self) -> usize { + self.id + } + + fn run_step(&mut self, step: Self::Step) -> StepTime { + use CarnotStepSolver::*; + match self.settings.steps_costs.get(&step) { + Some(Plain(t)) => *t, + Some(ParentCommitteeReceiverSolver(solver)) => solver( + &mut self.rng, + self.id, + self.settings + .layout + .parent_nodes(self.settings.layout.committee(self.id)), + &self.settings.network, + ), + Some(ChildCommitteeReceiverSolver(solver)) => solver( + &mut self.rng, + self.id, + &self + .settings + .layout + .children_nodes(self.settings.layout.committee(self.id)), + &self.settings.network, + ), + None => { + panic!("Unknown step: {step:?}"); + } + } + } +} diff --git a/simulations/src/node/mod.rs b/simulations/src/node/mod.rs new file mode 100644 index 00000000..51d675b3 --- /dev/null +++ b/simulations/src/node/mod.rs @@ -0,0 +1,19 @@ +pub mod carnot; + +// std +use std::time::Duration; +// crates +use rand::Rng; +// internal + +pub type NodeId = usize; +pub type CommitteeId = usize; +pub type StepTime = Duration; + +pub trait Node { + type Settings; + type Step; + fn new(rng: &mut R, id: NodeId, settings: Self::Settings) -> Self; + fn id(&self) -> NodeId; + fn run_step(&mut self, steps: Self::Step) -> StepTime; +} diff --git a/simulations/src/overlay/flat.rs b/simulations/src/overlay/flat.rs new file mode 100644 index 00000000..148d0f33 --- /dev/null +++ b/simulations/src/overlay/flat.rs @@ -0,0 +1,36 @@ +use rand::prelude::IteratorRandom; +use rand::Rng; +// std +// crates +// internal +use super::Overlay; +use crate::node::NodeId; +use crate::overlay::{Committee, Layout}; + +pub struct FlatOverlay; + +impl Overlay for FlatOverlay { + type Settings = (); + + fn new(_settings: Self::Settings) -> Self { + Self + } + + fn leaders( + &self, + nodes: &[NodeId], + size: usize, + rng: &mut R, + ) -> Box> { + let leaders = nodes.iter().copied().choose_multiple(rng, size).into_iter(); + Box::new(leaders) + } + + fn layout(&self, nodes: &[NodeId], _rng: &mut R) -> Layout { + let committees = + std::iter::once((0, nodes.iter().copied().collect::())).collect(); + let parent = std::iter::once((0, 0)).collect(); + let children = std::iter::once((0, vec![0])).collect(); + Layout::new(committees, parent, children) + } +} diff --git a/simulations/src/overlay/mod.rs b/simulations/src/overlay/mod.rs new file mode 100644 index 00000000..ec88db45 --- /dev/null +++ b/simulations/src/overlay/mod.rs @@ -0,0 +1,84 @@ +pub mod flat; + +// std +use std::collections::{BTreeSet, HashMap}; +// crates +use rand::Rng; +// internal +use crate::node::{CommitteeId, NodeId}; + +pub type Committee = BTreeSet; +pub type Leaders = BTreeSet; + +pub struct Layout { + pub committees: HashMap, + pub from_committee: HashMap, + pub parent: HashMap, + pub children: HashMap>, +} + +impl Layout { + pub fn new( + committees: HashMap, + parent: HashMap, + children: HashMap>, + ) -> Self { + let from_committee = committees + .iter() + .flat_map(|(&committee_id, committee)| { + committee + .iter() + .map(move |&node_id| (node_id, committee_id)) + }) + .collect(); + Self { + committees, + from_committee, + parent, + children, + } + } + + pub fn committee(&self, node_id: NodeId) -> CommitteeId { + self.from_committee.get(&node_id).copied().unwrap() + } + + pub fn committee_nodes(&self, committee_id: CommitteeId) -> &Committee { + &self.committees[&committee_id] + } + + pub fn parent(&self, committee_id: CommitteeId) -> CommitteeId { + self.parent[&committee_id] + } + + pub fn parent_nodes(&self, committee_id: CommitteeId) -> &Committee { + &self.committees[&self.parent(committee_id)] + } + + pub fn children(&self, committee_id: CommitteeId) -> &[CommitteeId] { + &self.children[&committee_id] + } + + pub fn children_nodes(&self, committee_id: CommitteeId) -> Vec<&Committee> { + self.children(committee_id) + .iter() + .map(|&committee_id| &self.committees[&committee_id]) + .collect() + } + + pub fn node_ids(&self) -> impl Iterator + '_ { + self.from_committee.keys().copied() + } +} + +pub trait Overlay { + type Settings; + fn new(settings: Self::Settings) -> Self; + fn leaders( + &self, + nodes: &[NodeId], + size: usize, + rng: &mut R, + ) -> Box>; + fn layout(&self, nodes: &[NodeId], rng: &mut R) -> Layout; +} diff --git a/simulations/src/runner.rs b/simulations/src/runner.rs new file mode 100644 index 00000000..e823eec8 --- /dev/null +++ b/simulations/src/runner.rs @@ -0,0 +1,214 @@ +// 1 - Leader forwards a proposal - Leader builds proposal +// 2 - Every committee member receives the proposal and validates it +// + +use crate::node::{Node, NodeId, StepTime}; +use crate::overlay::Layout; +use rand::Rng; +use std::time::Duration; + +pub struct ConsensusRunner { + nodes: Vec, + layout: Layout, + leaders: Vec, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct Report { + round_time: Duration, +} + +#[derive(Copy, Clone)] +pub enum LayoutNodes { + Leader, + Committee, + LeafCommittee, +} + +pub type ExecutionSteps = [(LayoutNodes, S, Box StepTime>)]; + +impl ConsensusRunner +where + N::Settings: Clone, +{ + pub fn new( + mut rng: R, + layout: Layout, + leaders: Vec, + node_settings: N::Settings, + ) -> Self { + let nodes = layout + .node_ids() + .map(|id| N::new(&mut rng, id, node_settings.clone())) + .collect(); + Self { + nodes, + layout, + leaders, + } + } + + pub fn node_ids(&self) -> impl Iterator + '_ { + self.nodes.iter().map(Node::id) + } + + pub fn run(&mut self, execution: &ExecutionSteps) -> Report + where + N::Step: Clone, + { + let leaders = &self.leaders; + let layout = &self.layout; + + let round_time = execution + .iter() + .map(|(layout_node, step, reducer)| { + let times: Vec = match layout_node { + LayoutNodes::Leader => leaders + .iter() + .map(|&leader| self.nodes[leader].run_step(step.clone())) + .collect(), + LayoutNodes::Committee => { + let non_leaf_committees = layout + .children + .iter() + .filter_map(|(id, children)| (!children.is_empty()).then_some(id)); + + non_leaf_committees + .flat_map(|committee_id| { + layout + .committees + .get(committee_id) + .unwrap() + .iter() + .map(|&node| self.nodes[node].run_step(step.clone())) + .max() + }) + .collect() + } + LayoutNodes::LeafCommittee => { + let leaf_committees = + layout.children.iter().filter_map(|(id, children)| { + (children.is_empty() || (layout.parent(*id) == *id)).then_some(id) + }); + + leaf_committees + .flat_map(|committee_id| { + layout + .committees + .get(committee_id) + .unwrap() + .iter() + .map(|&node| self.nodes[node].run_step(step.clone())) + .max() + }) + .collect() + } + }; + + reducer(×) + }) + .sum(); + + Report { round_time } + } +} + +#[cfg(test)] +mod test { + use crate::network::behaviour::NetworkBehaviour; + use crate::network::regions::{Region, RegionsData}; + use crate::network::Network; + use crate::node::carnot::{ + CarnotNode, CarnotNodeSettings, CARNOT_LEADER_STEPS, CARNOT_LEAF_STEPS, CARNOT_STEPS_COSTS, + }; + use crate::node::{NodeId, StepTime}; + use crate::overlay::flat::FlatOverlay; + use crate::overlay::Overlay; + use crate::runner::{ConsensusRunner, LayoutNodes}; + use rand::rngs::SmallRng; + use rand::{Rng, SeedableRng}; + use std::rc::Rc; + use std::time::Duration; + + fn setup_runner( + mut rng: &mut R, + overlay: &O, + ) -> ConsensusRunner { + let regions = std::iter::once((Region::Europe, (0..10).collect())).collect(); + let network_behaviour = std::iter::once(( + (Region::Europe, Region::Europe), + NetworkBehaviour::new(Duration::from_millis(100), 0.0), + )) + .collect(); + let node_ids: Vec = (0..10).collect(); + let layout = overlay.layout(&node_ids, &mut rng); + let leaders = overlay.leaders(&node_ids, 1, &mut rng).collect(); + let node_settings: CarnotNodeSettings = CarnotNodeSettings { + steps_costs: CARNOT_STEPS_COSTS.iter().cloned().collect(), + network: Network::new(RegionsData::new(regions, network_behaviour)), + layout: overlay.layout(&node_ids, &mut rng), + }; + + ConsensusRunner::new(&mut rng, layout, leaders, Rc::new(node_settings)) + } + + #[test] + fn test_run_flat_single_leader_steps() { + let mut rng = SmallRng::seed_from_u64(0); + let overlay = FlatOverlay::new(()); + + let mut runner = setup_runner(&mut rng, &overlay); + + let carnot_steps: Vec<_> = CARNOT_LEADER_STEPS + .iter() + .copied() + .map(|step| { + ( + LayoutNodes::Leader, + step, + Box::new(|times: &[StepTime]| *times.iter().max().unwrap()) + as Box StepTime>, + ) + }) + .collect(); + + assert_eq!( + Duration::from_millis(1100), + runner.run(&carnot_steps).round_time + ); + } + + #[test] + fn test_run_flat_single_leader_single_committee() { + let mut rng = SmallRng::seed_from_u64(0); + let overlay = FlatOverlay::new(()); + + let mut runner: ConsensusRunner = setup_runner(&mut rng, &overlay); + + let leader_steps = CARNOT_LEADER_STEPS.iter().copied().map(|step| { + ( + LayoutNodes::Leader, + step, + Box::new(|times: &[StepTime]| *times.iter().max().unwrap()) + as Box StepTime>, + ) + }); + + let committee_steps = CARNOT_LEAF_STEPS.iter().copied().map(|step| { + ( + LayoutNodes::LeafCommittee, + step, + Box::new(|times: &[StepTime]| *times.iter().max().unwrap()) + as Box StepTime>, + ) + }); + + let carnot_steps: Vec<_> = leader_steps.chain(committee_steps).collect(); + + assert_eq!( + Duration::from_millis(2200), + runner.run(&carnot_steps).round_time + ); + } +}