[WIP] Simulation app barebones (#90)

* Add simulations crate

* Kickstart modules

* Add overlay

* Use node steps

* Add initial carnot node

* Implement Carnot node costs simulation

* Firs working runner

* Cleanup

* Extract overlay from runner

* Fix off id calls

* Fix testing values

* Clippy happy

* make simulation app compile to wasm

---------

Co-authored-by: al8n <scygliu1@gmail.com>
This commit is contained in:
Daniel Sanchez 2023-03-14 07:15:45 -07:00 committed by GitHub
parent 274e8d55fd
commit 6ce9fdf553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 648 additions and 1 deletions

View File

@ -9,5 +9,6 @@ members = [
"nomos-services/mempool",
"nomos-services/http",
"nodes/nomos-node",
"nodes/mockpool-node"
"nodes/mockpool-node",
"simulations"
]

15
simulations/Cargo.toml Normal file
View File

@ -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"] }

View File

@ -0,0 +1,3 @@
pub fn main() {
println!("WIP");
}

4
simulations/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod network;
pub mod node;
pub mod overlay;
pub mod runner;

View File

@ -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<R: Rng>(&self, rng: &mut R) -> bool {
rng.gen_bool(self.drop)
}
}

View File

@ -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<R: Rng>(
&self,
rng: &mut R,
node_a: NodeId,
node_b: NodeId,
) -> Option<Duration> {
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())
}
}

View File

@ -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<Region, Vec<NodeId>>,
#[serde(skip)]
pub node_region: HashMap<NodeId, Region>,
pub region_network_behaviour: HashMap<(Region, Region), NetworkBehaviour>,
}
impl RegionsData {
pub fn new(
regions: HashMap<Region, Vec<NodeId>>,
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[&region]
}
}

View File

@ -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<CarnotStep, CarnotStepSolver>,
pub network: Network,
pub layout: Layout,
}
pub struct CarnotNode {
id: NodeId,
rng: SmallRng,
settings: Rc<CarnotNodeSettings>,
}
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<CarnotNodeSettings>;
type Step = CarnotStep;
fn new<R: Rng>(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:?}");
}
}
}
}

View File

@ -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<R: Rng>(rng: &mut R, id: NodeId, settings: Self::Settings) -> Self;
fn id(&self) -> NodeId;
fn run_step(&mut self, steps: Self::Step) -> StepTime;
}

View File

@ -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<R: Rng>(
&self,
nodes: &[NodeId],
size: usize,
rng: &mut R,
) -> Box<dyn Iterator<Item = NodeId>> {
let leaders = nodes.iter().copied().choose_multiple(rng, size).into_iter();
Box::new(leaders)
}
fn layout<R: Rng>(&self, nodes: &[NodeId], _rng: &mut R) -> Layout {
let committees =
std::iter::once((0, nodes.iter().copied().collect::<Committee>())).collect();
let parent = std::iter::once((0, 0)).collect();
let children = std::iter::once((0, vec![0])).collect();
Layout::new(committees, parent, children)
}
}

View File

@ -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<NodeId>;
pub type Leaders = BTreeSet<NodeId>;
pub struct Layout {
pub committees: HashMap<CommitteeId, Committee>,
pub from_committee: HashMap<NodeId, CommitteeId>,
pub parent: HashMap<CommitteeId, CommitteeId>,
pub children: HashMap<CommitteeId, Vec<CommitteeId>>,
}
impl Layout {
pub fn new(
committees: HashMap<CommitteeId, Committee>,
parent: HashMap<CommitteeId, CommitteeId>,
children: HashMap<CommitteeId, Vec<CommitteeId>>,
) -> 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<Item = NodeId> + '_ {
self.from_committee.keys().copied()
}
}
pub trait Overlay {
type Settings;
fn new(settings: Self::Settings) -> Self;
fn leaders<R: Rng>(
&self,
nodes: &[NodeId],
size: usize,
rng: &mut R,
) -> Box<dyn Iterator<Item = NodeId>>;
fn layout<R: Rng>(&self, nodes: &[NodeId], rng: &mut R) -> Layout;
}

214
simulations/src/runner.rs Normal file
View File

@ -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<N> {
nodes: Vec<N>,
layout: Layout,
leaders: Vec<NodeId>,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct Report {
round_time: Duration,
}
#[derive(Copy, Clone)]
pub enum LayoutNodes {
Leader,
Committee,
LeafCommittee,
}
pub type ExecutionSteps<S> = [(LayoutNodes, S, Box<dyn Fn(&[StepTime]) -> StepTime>)];
impl<N: Node> ConsensusRunner<N>
where
N::Settings: Clone,
{
pub fn new<R: Rng>(
mut rng: R,
layout: Layout,
leaders: Vec<NodeId>,
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<Item = NodeId> + '_ {
self.nodes.iter().map(Node::id)
}
pub fn run(&mut self, execution: &ExecutionSteps<N::Step>) -> 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<StepTime> = 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(&times)
})
.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<R: Rng, O: Overlay>(
mut rng: &mut R,
overlay: &O,
) -> ConsensusRunner<CarnotNode> {
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<NodeId> = (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<dyn Fn(&[StepTime]) -> 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<CarnotNode> = 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<dyn Fn(&[StepTime]) -> StepTime>,
)
});
let committee_steps = CARNOT_LEAF_STEPS.iter().copied().map(|step| {
(
LayoutNodes::LeafCommittee,
step,
Box::new(|times: &[StepTime]| *times.iter().max().unwrap())
as Box<dyn Fn(&[StepTime]) -> StepTime>,
)
});
let carnot_steps: Vec<_> = leader_steps.chain(committee_steps).collect();
assert_eq!(
Duration::from_millis(2200),
runner.run(&carnot_steps).round_time
);
}
}