diff --git a/crates/maybenot-simulator/src/lib.rs b/crates/maybenot-simulator/src/lib.rs index d66cc6a..8faed78 100644 --- a/crates/maybenot-simulator/src/lib.rs +++ b/crates/maybenot-simulator/src/lib.rs @@ -45,7 +45,7 @@ //! //! // A simple machine that sends one padding packet 20 milliseconds after the //! // first normal packet is sent. -//! let m = "02eNptibEJAAAIw1of09Mc/c+HRMFFzFBoAlxkliTgurLfT6T9oQBWJgJi"; +//! let m = "02eNp1ibEJAEAIA5Nf7B3N0v1cSESwEL0m5A6YvBqSgP7WeXfM5UoBW7ICYg=="; //! let m = Machine::from_str(m).unwrap(); //! //! // Run the simulator with the machine at the client. Run the simulation up diff --git a/crates/maybenot-simulator/tests/example.rs b/crates/maybenot-simulator/tests/example.rs index 282d00f..eb7492f 100644 --- a/crates/maybenot-simulator/tests/example.rs +++ b/crates/maybenot-simulator/tests/example.rs @@ -34,7 +34,7 @@ fn simple_machine_for_example() { let m = Machine::new(0, 0.0, 0, 0.0, vec![s0, s1]).unwrap(); assert_eq!( m.serialize(), - "02eNptibEJAAAIw1of09Mc/c+HRMFFzFBoAlxkliTgurLfT6T9oQBWJgJi" + "02eNp1ibEJAEAIA5Nf7B3N0v1cSESwEL0m5A6YvBqSgP7WeXfM5UoBW7ICYg==" ); } @@ -67,7 +67,7 @@ fn simulator_example_use() { // A simple machine that sends one padding packet 20 milliseconds after the // first normal packet is sent. - let m = "02eNptibEJAAAIw1of09Mc/c+HRMFFzFBoAlxkliTgurLfT6T9oQBWJgJi"; + let m = "02eNp1ibEJAEAIA5Nf7B3N0v1cSESwEL0m5A6YvBqSgP7WeXfM5UoBW7ICYg=="; let m = Machine::from_str(m).unwrap(); // Run the simulator with the machine at the client. Run the simulation up diff --git a/crates/maybenot-simulator/tests/simulator.rs b/crates/maybenot-simulator/tests/simulator.rs index 0049736..c4b8585 100644 --- a/crates/maybenot-simulator/tests/simulator.rs +++ b/crates/maybenot-simulator/tests/simulator.rs @@ -6,7 +6,7 @@ use std::time::Duration; use maybenot::{ action::Action, - counter::{Counter, CounterUpdate, Operation}, + counter::{Counter, Operation}, dist::{Dist, DistType}, event::Event, state::{State, Trans}, @@ -1086,44 +1086,44 @@ fn test_counter_machine() { Event::NormalRecv => vec![Trans(2, 1.0)], _ => vec![], }); - s1.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Increment, - value: Some(Dist { - dist: DistType::Uniform { - low: 5.0, - high: 5.0, + s1.counter = ( + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 5.0, + high: 5.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + None, + ); let mut s2 = State::new(enum_map! { Event::NormalRecv => vec![Trans(3, 1.0)], _ => vec![], }); - s2.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Decrement, - value: Some(Dist { - dist: DistType::Uniform { - low: 2.0, - high: 2.0, + s2.counter = ( + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 2.0, + high: 2.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + None, + ); let mut s3 = State::new(enum_map! { Event::NormalSent => vec![Trans(3, 1.0)], Event::CounterZero => vec![Trans(4, 1.0)], _ => vec![], }); - s3.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Decrement, - value: None, // same as 1 - }); + s3.counter = (Some(Counter::new(Operation::Decrement)), None); let mut s4 = State::new(enum_map! { _ => vec![], }); @@ -1153,13 +1153,9 @@ fn test_counter_machine() { false, ); - // set counter in state 3 to Counter::B, to prevent the CounterZero event + // set counter in state 3 to Counter B, to prevent the CounterZero event // from firing - m.states[3].counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Decrement, - value: None, - }); + m.states[3].counter = (None, Some(Counter::new(Operation::Decrement))); run_test_sim( "0,sn 6,rn 6,rn 7,sn 7,sn 7,sn", "0,sn 0,st 6,rt 6,rt 6,rn 6,rn 7,sn 7,st 7,sn 7,st 7,sn 7,st", @@ -1171,31 +1167,35 @@ fn test_counter_machine() { false, ); - // update state 1 and 2 to also use Counter::B - m.states[1].counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Increment, - value: Some(Dist { - dist: DistType::Uniform { - low: 5.0, - high: 5.0, + // update state 1 and 2 to also use Counter B + m.states[1].counter = ( + None, + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 5.0, + high: 5.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); - m.states[2].counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Decrement, - value: Some(Dist { - dist: DistType::Uniform { - low: 2.0, - high: 2.0, + )), + ); + m.states[2].counter = ( + None, + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 2.0, + high: 2.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); run_test_sim( "0,sn 6,rn 6,rn 7,sn 7,sn 7,sn", "0,sn 0,st 6,rt 6,rt 6,rn 6,rn 7,sn 7,st 7,sn 7,st 7,sn 7,st 10,sp 10,st", @@ -1208,18 +1208,20 @@ fn test_counter_machine() { ); // replace increment in state 1 with set operation, should make no difference - m.states[1].counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Set, - value: Some(Dist { - dist: DistType::Uniform { - low: 5.0, - high: 5.0, + m.states[1].counter = ( + None, + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 5.0, + high: 5.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); run_test_sim( "0,sn 6,rn 6,rn 7,sn 7,sn 7,sn", "0,sn 0,st 6,rt 6,rt 6,rn 6,rn 7,sn 7,st 7,sn 7,st 7,sn 7,st 10,sp 10,st", diff --git a/crates/maybenot/CHANGELOG.md b/crates/maybenot/CHANGELOG.md index f95a0cb..a493f5e 100644 --- a/crates/maybenot/CHANGELOG.md +++ b/crates/maybenot/CHANGELOG.md @@ -20,13 +20,18 @@ Manually generated changelog, for now. We follow semantic versioning. machines and effectively dealing with blocking actions. - Added two counters per machine which are updated upon transition to a state if its `counter` field is set. A `CounterZero` event is triggered when either of - a machine's counters is decremented to zero. + a machine's counters is decremented to zero. Counters are internal to the + framework and are not exposed to the integrator. - Added a per-machine "internal" timer which can be set using an `UpdateTimer` - action. These are handled by the integrator, who triggers the corresponding - `TimerBegin` and `TimerEnd` events as the timer starts and fire. + action. These are handled by the integrator (to not impose any particular + runtime for timers), who triggers the corresponding `TimerBegin` and + `TimerEnd` events as the timer starts and fire. - Extended the `Cancel` action that can be used to cancel a pending action timer (timeout), the machine's internal timer, or both. The internal pseudo-state `STATE_CANCEL` transition is removed. +- Added support for `Event::Signal`, allowing machines to signal between each + other. Useful for multiple machines that need to coordinate their states. This + is internal to the framework and is not exposed to the integrator. - Added support for the `SkewNormal` distribution. - Added an optional `parsing` feature to reconstruct v1 machines, though they may behave differently than expected. v1 machines are now deprecated. diff --git a/crates/maybenot/src/constants.rs b/crates/maybenot/src/constants.rs index 9082ea8..aab49b5 100644 --- a/crates/maybenot/src/constants.rs +++ b/crates/maybenot/src/constants.rs @@ -9,7 +9,7 @@ pub const VERSION: u8 = 2; pub const MAX_DECOMPRESSED_SIZE: usize = 1 << 20; /// The number of [`Event`](crate::event)s in the framework. -pub const EVENT_NUM: usize = 12; +pub const EVENT_NUM: usize = 13; /// The maximum sampled timeout in a [`State`](crate::state), set to a day in /// microseconds. @@ -38,6 +38,9 @@ pub const STATE_NOP: usize = u32::MAX as usize; /// A pseudo-state that means the [`Machine`](crate::Machine) should completely /// stop. pub const STATE_END: usize = STATE_NOP - 1; +/// A pseudo-state that triggers a Signal [`Event`](crate::event) in all other +/// running machines. +pub const STATE_SIGNAL: usize = STATE_END - 1; /// The maximum number of [`State`](crate::state)s a [`Machine`](crate::Machine) /// can have. -pub const STATE_MAX: usize = STATE_END - 1; +pub const STATE_MAX: usize = STATE_SIGNAL - 1; diff --git a/crates/maybenot/src/counter.rs b/crates/maybenot/src/counter.rs index bf15a16..f48f78f 100644 --- a/crates/maybenot/src/counter.rs +++ b/crates/maybenot/src/counter.rs @@ -9,62 +9,92 @@ use std::fmt; use self::dist::Dist; -/// The two counters that are part of each [`Machine`]. -#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] -pub enum Counter { - A, - B, -} - -/// The operation applied to a [`Machine`]'s counters upon -/// transition to a [`State`](crate::state::State). +/// The operation applied to one of a [`Machine`]'s counters upon transition to +/// a [`State`](crate::state::State). #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] pub enum Operation { - /// Increment the counter by the sampled value. + /// Increment the counter. Increment, - /// Decrement the counter by the sampled value. + /// Decrement the counter. Decrement, - /// Replace the current value of the counter with the sampled one. + /// Replace the current value of the counter. Set, } -/// A specification of how a [`Machine`]'s counters should be -/// updated when transitioning to a [`State`](crate::state::State). Consists of -/// a [`Counter`], an [`Operation`] to be applied to the counter, and a -/// distribution to sample values from when updating the counter. +/// A specification of how one of a [`Machine`]'s counters should be updated +/// when transitioning to a [`State`](crate::state::State). Consists of an +/// [`Operation`] to be applied to the counter with one of three values: by +/// default, the value 1, unless a distribution is provided or the copy flag is +/// set to true. If the copy flag is set to true, the counter will be updated +/// with the value of the other counter *prior to transitioning to the state*. +/// If a distribution is provided, the counter will be updated with a value +/// sampled from the distribution. #[derive(PartialEq, Debug, Clone, Copy, Serialize, Deserialize)] -pub struct CounterUpdate { - /// Which counter to update. - pub counter: Counter, - /// The operation to apply to the counter upon a state transition. +pub struct Counter { + /// The operation to apply to the counter upon a state transition. If the + /// distribution is not set and copy is false, the counter will be updated + /// by 1. pub operation: Operation, /// If set, sample the value to update the counter with from a - /// distribution. If not set, value is 1. - pub value: Option, + /// distribution. + pub dist: Option, + /// If set, the counter will be updated by the other counter's value *prior + /// to transitioning to the state*. Supersedes the `dist` field. + pub copy: bool, } -impl fmt::Display for CounterUpdate { +impl fmt::Display for Counter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:#?}", self) } } -impl CounterUpdate { +impl Counter { + /// Create a new counter with an operation that modifies the counter with + /// value 1. + pub fn new(operation: Operation) -> Self { + Counter { + operation, + dist: None, + copy: false, + } + } + + /// Create a new counter with an operation and a distribution to sample the + /// value from. + pub fn new_dist(operation: Operation, dist: Dist) -> Self { + Counter { + operation, + dist: Some(dist), + copy: false, + } + } + + /// Create a new counter with an operation that copies the value of the + /// other counter *prior to transitioning to the state*. + pub fn new_copy(operation: Operation) -> Self { + Counter { + operation, + dist: None, + copy: true, + } + } + /// Sample a value to update the counter with. pub fn sample_value(&self, rng: &mut R) -> u64 { - match self.value { - Some(value) => { - let s = value.sample(rng) as u64; + match self.dist { + None => 1, + Some(dist) => { + let s = dist.sample(rng) as u64; s.min(MAX_SAMPLED_COUNTER_VALUE) } - None => 1, } } // Validate the value dist. pub fn validate(&self) -> Result<(), Error> { - if let Some(value) = self.value { - value.validate()?; + if let Some(dist) = self.dist { + dist.validate()?; } Ok(()) } @@ -77,24 +107,23 @@ mod tests { #[test] fn validate_counter_update() { // valid counter update - let mut cu = CounterUpdate { - counter: Counter::A, - operation: Operation::Increment, - value: Some(Dist { + let mut cu = Counter::new_dist( + Operation::Increment, + Dist { dist: DistType::Uniform { low: 10.0, high: 10.0, }, start: 0.0, max: 0.0, - }), - }; + }, + ); let r = cu.validate(); assert!(r.is_ok()); // counter update with invalid dist - cu.value = Some(Dist { + cu.dist = Some(Dist { dist: DistType::Uniform { low: 15.0, // NOTE low > high high: 5.0, @@ -106,12 +135,18 @@ mod tests { let r = cu.validate(); assert!(r.is_err()); - // counter with empty dist - cu.value = None; + // counter with default value + cu.dist = None; let r = cu.validate(); assert!(r.is_ok()); assert_eq!(cu.sample_value(&mut rand::thread_rng()), 1); + + // counter with copy value + cu.copy = true; + + let r = cu.validate(); + assert!(r.is_ok()); } } diff --git a/crates/maybenot/src/event.rs b/crates/maybenot/src/event.rs index 9d4b08e..991b6c6 100644 --- a/crates/maybenot/src/event.rs +++ b/crates/maybenot/src/event.rs @@ -38,6 +38,8 @@ pub enum Event { TimerBegin, /// TimerEnd is when a machine's timer expired. TimerEnd, + /// Signal is when a machine transitioned to [`STATE_SIGNAL`](crate::constants). + Signal, } impl fmt::Display for Event { @@ -61,6 +63,7 @@ impl Event { CounterZero, TimerBegin, TimerEnd, + Signal, ]; EVENTS.iter() } @@ -153,5 +156,6 @@ mod tests { assert_eq!(Event::TimerEnd.to_string(), "TimerEnd"); assert_eq!(Event::TunnelRecv.to_string(), "TunnelRecv"); assert_eq!(Event::TunnelSent.to_string(), "TunnelSent"); + assert_eq!(Event::Signal.to_string(), "Signal"); } } diff --git a/crates/maybenot/src/framework.rs b/crates/maybenot/src/framework.rs index 12eab9a..7b754ee 100644 --- a/crates/maybenot/src/framework.rs +++ b/crates/maybenot/src/framework.rs @@ -6,9 +6,7 @@ use rand_core::RngCore; use crate::*; use self::action::Action; -use self::constants::STATE_END; -use self::constants::STATE_LIMIT_MAX; -use self::counter::Counter; +use self::constants::{STATE_END, STATE_LIMIT_MAX, STATE_SIGNAL}; use self::counter::Operation; use self::event::Event; use crate::time::Duration as _; @@ -87,6 +85,10 @@ where blocking_duration: T::Duration, blocking_started: T, blocking_active: bool, + // internal signaling: if None then we don't signal, Some(None) means all + // machines receive the signal, Some(Some(...)) means one machine doesn't + // (the one that sent the signal) + signal_pending: Option>, framework_start: T, } @@ -156,6 +158,7 @@ where blocking_duration: T::Duration::zero(), padding_sent_packets: 0, normal_sent_packets: 0, + signal_pending: None, }; for (runtime, machine) in s.runtime.iter_mut().zip(s.machines.as_ref().iter()) { @@ -205,6 +208,30 @@ where self.current_time = current_time; for e in events { self.process_event(e); + + // handle internal signaling + // Special case: if a machine sends a signal which results in another + // signal in response, that response may or may not be received; this + // depends on the order in which machines are given to the framework. + // Check for this explicitly (keep track of excluded machines). + if let Some(signal) = self.signal_pending { + let mut excluded = None; + + for mi in 0..self.runtime.len() { + if signal.is_some() && signal.unwrap() == mi { + excluded = Some(mi); + continue; + } + self.transition(mi, Event::Signal); + } + + if let Some(excluded) = excluded { + if self.signal_pending.unwrap().is_none() { + self.transition(excluded, Event::Signal); + } + } + self.signal_pending = None; + } } // only return actions, no None @@ -349,6 +376,18 @@ where self.runtime[mi].current_state = STATE_END; StateChange::Changed } + STATE_SIGNAL => { + // this is not a state change, just signal *other* machines + self.signal_pending = match self.signal_pending { + // no signal pending, so signal all *other* machines + None => Some(Some(mi)), + // signal pending from another machine, so signal all + // machines (including this one) by removing any machine + // index set + _ => Some(None), + }; + StateChange::Unchanged + } _ => { // transition to same or different state? let state_changed = if self.runtime[mi].current_state == next_state { @@ -385,36 +424,69 @@ where } fn update_counter(&mut self, mi: usize) -> (StateChange, bool) { - let current = &self.machines.as_ref()[mi].states[self.runtime[mi].current_state]; + let state = &self.machines.as_ref()[mi].states[self.runtime[mi].current_state]; + + let old_value_a = self.runtime[mi].counter_a; + let old_value_b = self.runtime[mi].counter_b; + let mut any_counter_zeroed = false; - if let Some(update) = ¤t.counter { - let value = update.sample_value(&mut self.rng); - let counter = if update.counter == Counter::A { - &mut self.runtime[mi].counter_a + // counter A and B are independent, so we update them separately + if let Some(counter_a) = state.counter.0 { + let change = if counter_a.copy { + old_value_b } else { - &mut self.runtime[mi].counter_b + counter_a.sample_value(&mut self.rng) }; - let already_zero = *counter == 0; - match update.operation { + let updated_value_a = &mut self.runtime[mi].counter_a; + match counter_a.operation { Operation::Increment => { - *counter = counter.saturating_add(value); + *updated_value_a = updated_value_a.saturating_add(change); } Operation::Decrement => { - *counter = counter.saturating_sub(value); + *updated_value_a = updated_value_a.saturating_sub(change); } Operation::Set => { - *counter = value; + *updated_value_a = change; } } - if *counter == 0 && !already_zero { - self.actions[mi] = None; - let result = self.transition(mi, Event::CounterZero); - return (result, true); + if old_value_a != 0 && *updated_value_a == 0 { + any_counter_zeroed = true; } } - // Do nothing if counter value is unchanged or not zero + + if let Some(counter_b) = state.counter.1 { + let change = if counter_b.copy { + old_value_a + } else { + counter_b.sample_value(&mut self.rng) + }; + + let updated_value_b = &mut self.runtime[mi].counter_b; + match counter_b.operation { + Operation::Increment => { + *updated_value_b = updated_value_b.saturating_add(change); + } + Operation::Decrement => { + *updated_value_b = updated_value_b.saturating_sub(change); + } + Operation::Set => { + *updated_value_b = change; + } + } + + if old_value_b != 0 && *updated_value_b == 0 { + any_counter_zeroed = true; + } + } + + if any_counter_zeroed { + self.actions[mi] = None; + return (self.transition(mi, Event::CounterZero), true); + } + + // do nothing if counter value is unchanged or not zero (StateChange::Unchanged, false) } @@ -583,7 +655,7 @@ where #[cfg(test)] mod tests { - use crate::counter::CounterUpdate; + use crate::counter::Counter; use crate::dist::*; use crate::framework::*; use crate::state::*; @@ -614,7 +686,7 @@ mod tests { _ => vec![], }); let m = Machine::new(0, 0.0, 0, 0.0, vec![s0]).unwrap(); - assert_eq!(m.serialize(), "02eNpjYEAHjKhcAAAwAAI="); + assert_eq!(m.serialize(), "02eNpjYEAHjOgCAAA0AAI="); } #[test] @@ -957,8 +1029,8 @@ mod tests { #[test] fn counter_machine() { - // a machine that counts PaddingSent - NormalSent - // use counter A for that, pad and increment counter B on CounterZero + // count PaddingSent - NormalSent with counter A + // pad and increment counter B by 4 on CounterZero // state 0 let mut s0 = State::new(enum_map! { @@ -966,36 +1038,14 @@ mod tests { Event::CounterZero => vec![Trans(2, 1.0)], _ => vec![], }); - s0.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Decrement, - value: Some(Dist { - dist: DistType::Uniform { - low: 1.0, - high: 1.0, - }, - start: 0.0, - max: 0.0, - }), - }); + s0.counter = (Some(Counter::new(Operation::Decrement)), None); // state 1 let mut s1 = State::new(enum_map! { Event::NormalSent => vec![Trans(0, 1.0)], _ => vec![], }); - s1.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Increment, - value: Some(Dist { - dist: DistType::Uniform { - low: 1.0, - high: 1.0, - }, - start: 0.0, - max: 0.0, - }), - }); + s1.counter = (Some(Counter::new(Operation::Increment)), None); // state 2 let mut s2 = State::new(enum_map! { @@ -1016,18 +1066,20 @@ mod tests { }, limit: None, }); - s2.counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Increment, - value: Some(Dist { - dist: DistType::Uniform { - low: 4.0, - high: 4.0, + s2.counter = ( + None, + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 4.0, + high: 4.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); // machine let m = Machine::new(1000, 1.0, 0, 0.0, vec![s0, s1, s2]).unwrap(); @@ -1072,18 +1124,21 @@ mod tests { Event::CounterZero => vec![Trans(2, 1.0)], _ => vec![], }); - s0.counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Decrement, // NOTE - value: Some(Dist { - dist: DistType::Uniform { - low: 10.0, - high: 10.0, + s0.counter = ( + None, + // NOTE decrement + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 10.0, + high: 10.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); // state 1, set counter let mut s1 = State::new(enum_map! { @@ -1092,18 +1147,20 @@ mod tests { Event::CounterZero => vec![Trans(2, 1.0)], _ => vec![], }); - s1.counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Set, - value: Some(Dist { - dist: DistType::Uniform { - low: 0.0, // NOTE - high: 0.0, + s1.counter = ( + None, + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 0.0, // NOTE + high: 0.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); // state 2, pad let mut s2 = State::new(enum_map! { @@ -1154,18 +1211,21 @@ mod tests { Event::NormalRecv => vec![Trans(1, 1.0)], _ => vec![], }); - s0.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Increment, // NOTE - value: Some(Dist { - dist: DistType::Uniform { - low: 1000.0, - high: 1000.0, + s0.counter = ( + // NOTE increment + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 1000.0, + high: 1000.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + None, + ); // state 1, set counter let mut s1 = State::new(enum_map! { @@ -1173,18 +1233,20 @@ mod tests { Event::NormalRecv => vec![Trans(1, 1.0)], _ => vec![], }); - s1.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Set, - value: Some(Dist { - dist: DistType::Uniform { - low: u64::MAX as f64, // NOTE - high: u64::MAX as f64, + s1.counter = ( + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: u64::MAX as f64, // NOTE + high: u64::MAX as f64, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + None, + ); // machine let m = Machine::new(1000, 1.0, 0, 0.0, vec![s0, s1]).unwrap(); @@ -1202,6 +1264,468 @@ mod tests { assert_eq!(f.runtime[0].counter_a, u64::MAX); } + #[test] + fn counter_convergence_machine() { + // set and decrement both counters at once, check correctness + // then decrement both to zero and ensure only one transition + + // state 0 + let mut s0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(0, 1.0)], + Event::PaddingSent => vec![Trans(1, 1.0)], + _ => vec![], + }); + s0.counter = ( + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 44.0, + high: 44.0, + }, + start: 0.0, + max: 0.0, + }, + )), + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 28.0, + high: 28.0, + }, + start: 0.0, + max: 0.0, + }, + )), + ); + + // state 1 + let mut s1 = State::new(enum_map! { + Event::NormalSent => vec![Trans(1, 1.0)], + Event::CounterZero => vec![Trans(2, 1.0)], + _ => vec![], + }); + s1.counter = ( + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 32.0, + high: 32.0, + }, + start: 0.0, + max: 0.0, + }, + )), + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 15.0, + high: 15.0, + }, + start: 0.0, + max: 0.0, + }, + )), + ); + + // state 2 + let s2 = State::new(enum_map! { + Event::CounterZero => vec![Trans(0, 1.0)], + _ => vec![], + }); + + // machine + let m = Machine::new(1000, 1.0, 0, 0.0, vec![s0, s1, s2]).unwrap(); + + let mut current_time = Instant::now(); + let machines = vec![m]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 44); + assert_eq!(f.runtime[0].counter_b, 28); + + current_time = current_time.add(Duration::from_micros(20)); + _ = f.trigger_events( + &[TriggerEvent::PaddingSent { + machine: MachineId(0), + }], + current_time, + ); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 12); + assert_eq!(f.runtime[0].counter_b, 13); + + current_time = current_time.add(Duration::from_micros(20)); + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 0); + assert_eq!(f.runtime[0].counter_b, 0); + } + + #[test] + fn counter_divergence_machine() { + // set both counters at once, check correctness + // decrement only one to zero and ensure CounterZero + + // state 0 + let mut s0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(0, 1.0)], + Event::PaddingSent => vec![Trans(1, 1.0)], + _ => vec![], + }); + s0.counter = ( + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 50.0, + high: 50.0, + }, + start: 0.0, + max: 0.0, + }, + )), + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 50.0, + high: 50.0, + }, + start: 0.0, + max: 0.0, + }, + )), + ); + + // state 1 + let mut s1 = State::new(enum_map! { + Event::CounterZero => vec![Trans(2, 1.0)], + _ => vec![], + }); + s1.counter = ( + None, + Some(Counter::new_dist( + Operation::Decrement, + Dist { + dist: DistType::Uniform { + low: 50.0, + high: 50.0, + }, + start: 0.0, + max: 0.0, + }, + )), + ); + + // state 2 + let mut s2 = State::new(enum_map! { + _ => vec![], + }); + s2.counter = ( + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 25.0, + high: 25.0, + }, + start: 0.0, + max: 0.0, + }, + )), + None, + ); + + // machine + let m = Machine::new(1000, 1.0, 0, 0.0, vec![s0, s1, s2]).unwrap(); + + let mut current_time = Instant::now(); + let machines = vec![m]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 50); + assert_eq!(f.runtime[0].counter_b, 50); + + current_time = current_time.add(Duration::from_micros(20)); + _ = f.trigger_events( + &[TriggerEvent::PaddingSent { + machine: MachineId(0), + }], + current_time, + ); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 75); + assert_eq!(f.runtime[0].counter_b, 0); + } + + #[test] + fn counter_copy_machine() { + // set both counters at once, then copy their values + + // state 0 + let mut s0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(0, 1.0)], + Event::PaddingSent => vec![Trans(1, 1.0)], + _ => vec![], + }); + s0.counter = ( + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 4.0, + high: 4.0, + }, + start: 0.0, + max: 0.0, + }, + )), + Some(Counter::new_dist( + Operation::Set, + Dist { + dist: DistType::Uniform { + low: 13.0, + high: 13.0, + }, + start: 0.0, + max: 0.0, + }, + )), + ); + + // state 1 + let mut s1 = State::new(enum_map! { + Event::PaddingSent => vec![Trans(2, 1.0)], + _ => vec![], + }); + s1.counter = ( + // should be 13.0 + Some(Counter::new_copy(Operation::Set)), + // should be 9.0 + Some(Counter::new_copy(Operation::Decrement)), + ); + + // state 2 + let mut s2 = State::new(enum_map! { + _ => vec![], + }); + s2.counter = ( + // should be 22.0 + Some(Counter::new_copy(Operation::Increment)), + // should still be 9.0 + None, + ); + + // machine + let m = Machine::new(1000, 1.0, 0, 0.0, vec![s0, s1, s2]).unwrap(); + + let mut current_time = Instant::now(); + let machines = vec![m]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 4); + assert_eq!(f.runtime[0].counter_b, 13); + + current_time = current_time.add(Duration::from_micros(20)); + _ = f.trigger_events( + &[TriggerEvent::PaddingSent { + machine: MachineId(0), + }], + current_time, + ); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 13); + assert_eq!(f.runtime[0].counter_b, 9); + + current_time = current_time.add(Duration::from_micros(20)); + _ = f.trigger_events( + &[TriggerEvent::PaddingSent { + machine: MachineId(0), + }], + current_time, + ); + assert_eq!(f.actions[0], None); + assert_eq!(f.runtime[0].counter_a, 22); + assert_eq!(f.runtime[0].counter_b, 9); + } + + #[test] + fn signal_one_machine() { + // send a signal from one machine, ensure that the signal is received + // by another machine but not the originating machine + + // state 0 + let s0_m0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(STATE_SIGNAL, 1.0)], + Event::Signal => vec![Trans(1, 1.0)], + _ => vec![], + }); + let s0_m1 = State::new(enum_map! { + Event::Signal => vec![Trans(1, 1.0)], + _ => vec![], + }); + + // state 1 + let mut s1 = State::new(enum_map! { + _ => vec![], + }); + s1.action = Some(Action::SendPadding { + bypass: false, + replace: false, + timeout: Dist { + dist: DistType::Uniform { + low: 2.0, + high: 2.0, + }, + start: 0.0, + max: 0.0, + }, + limit: None, + }); + + // machines + let m0 = Machine::new(1000, 1.0, 0, 0.0, vec![s0_m0, s1.clone()]).unwrap(); + let m1 = Machine::new(1000, 1.0, 0, 0.0, vec![s0_m1, s1.clone()]).unwrap(); + + let current_time = Instant::now(); + let machines = vec![m0, m1]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!(f.actions[0], None); + assert_eq!( + f.actions[1], + Some(TriggerAction::SendPadding { + timeout: Duration::from_micros(2), + bypass: false, + replace: false, + machine: MachineId(1), + }) + ); + } + + #[test] + fn signal_two_machine() { + // send a signal from two machines, ensure that both get a signal + + // state 0 + let s0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(STATE_SIGNAL, 1.0)], + Event::Signal => vec![Trans(1, 1.0)], + _ => vec![], + }); + + // state 1 + let mut s1 = State::new(enum_map! { + _ => vec![], + }); + s1.action = Some(Action::SendPadding { + bypass: false, + replace: false, + timeout: Dist { + dist: DistType::Uniform { + low: 2.0, + high: 2.0, + }, + start: 0.0, + max: 0.0, + }, + limit: None, + }); + + // machines + let m0 = Machine::new(1000, 1.0, 0, 0.0, vec![s0.clone(), s1.clone()]).unwrap(); + let m1 = Machine::new(1000, 1.0, 0, 0.0, vec![s0.clone(), s1.clone()]).unwrap(); + + let current_time = Instant::now(); + let machines = vec![m0, m1]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!( + f.actions[0], + Some(TriggerAction::SendPadding { + timeout: Duration::from_micros(2), + bypass: false, + replace: false, + machine: MachineId(0), + }) + ); + assert_eq!( + f.actions[1], + Some(TriggerAction::SendPadding { + timeout: Duration::from_micros(2), + bypass: false, + replace: false, + machine: MachineId(1), + }) + ); + } + + #[test] + fn signal_response() { + // send a signal and respond to it, ensure that both machines get a signal + + // state 0 + let s0_m0 = State::new(enum_map! { + Event::NormalSent => vec![Trans(STATE_SIGNAL, 1.0)], + Event::Signal => vec![Trans(1, 1.0)], + _ => vec![], + }); + let s0_m1 = State::new(enum_map! { + Event::Signal => vec![Trans(STATE_SIGNAL, 1.0)], + _ => vec![], + }); + + // state 1 + let mut s1 = State::new(enum_map! { + _ => vec![], + }); + s1.action = Some(Action::SendPadding { + bypass: false, + replace: false, + timeout: Dist { + dist: DistType::Uniform { + low: 2.0, + high: 2.0, + }, + start: 0.0, + max: 0.0, + }, + limit: None, + }); + + // machines + let m0 = Machine::new(1000, 1.0, 0, 0.0, vec![s0_m0, s1.clone()]).unwrap(); + let m1 = Machine::new(1000, 1.0, 0, 0.0, vec![s0_m1, s1.clone()]).unwrap(); + + let current_time = Instant::now(); + let machines = vec![m0, m1]; + let mut f = Framework::new(&machines, 0.0, 0.0, current_time, rand::thread_rng()).unwrap(); + + _ = f.trigger_events(&[TriggerEvent::NormalSent], current_time); + assert_eq!( + f.actions[0], + Some(TriggerAction::SendPadding { + timeout: Duration::from_micros(2), + bypass: false, + replace: false, + machine: MachineId(0), + }) + ); + assert_eq!(f.actions[1], None); + } + #[test] fn machine_max_padding_frac() { // We create a machine that should be allowed to send 100 padding diff --git a/crates/maybenot/src/lib.rs b/crates/maybenot/src/lib.rs index 8841687..6ceed04 100644 --- a/crates/maybenot/src/lib.rs +++ b/crates/maybenot/src/lib.rs @@ -34,7 +34,7 @@ //! // of the framework for the same machines, then share the same vector //! // across framework instances. All runtime information is allocated //! // internally in the framework without modifying the machines. -//! let s = "02eNpjYEAHjKhcAAAwAAI="; +//! let s = "02eNpjYEAHjOgCAAA0AAI="; //! // machines will error if invalid //! let m = vec![Machine::from_str(s).unwrap()]; //! @@ -247,7 +247,7 @@ mod tests { // of the framework for the same machines, then share the same vector // across framework instances. All runtime information is allocated // internally in the framework without modifying the machines. - let s = "02eNpjYEAHjKhcAAAwAAI="; + let s = "02eNpjYEAHjOgCAAA0AAI="; // machines will error if invalid let m = vec![Machine::from_str(s).unwrap()]; diff --git a/crates/maybenot/src/state.rs b/crates/maybenot/src/state.rs index 1d87eec..60acfd3 100644 --- a/crates/maybenot/src/state.rs +++ b/crates/maybenot/src/state.rs @@ -1,6 +1,6 @@ -//! A state as part of a [`Machine`]. Contains an optional -//! [`Action`] and [`CounterUpdate`] to be executed upon transition to this -//! state, and a vector of state transitions for each possible [`Event`]. +//! A state as part of a [`Machine`]. Contains an optional [`Action`] and +//! [`Counter`] to be executed upon transition to this state, and a vector of +//! state transitions for each possible [`Event`]. use crate::constants::*; use crate::*; @@ -13,7 +13,7 @@ use std::collections::HashSet; use std::fmt; use self::action::Action; -use self::counter::CounterUpdate; +use self::counter::Counter; use self::event::Event; use enum_map::enum_map; @@ -27,7 +27,7 @@ impl fmt::Display for Trans { if self.1 == 1.0 { write!(f, "{}", self.0) } else { - write!(f, "{} ({})", self.0, self.1) + write!(f, "{} ({})", self.0, self.1) } } } @@ -37,8 +37,8 @@ impl fmt::Display for Trans { pub struct State { /// Take an action upon transitioning to this state. pub action: Option, - /// On transition to this state, update a machine counter. - pub counter: Option, + /// On transition to this state, update the machine's two counters (A,B). + pub counter: (Option, Option), /// For each possible [`Event`], a vector of state transitions. transitions: [Option>; EVENT_NUM], } @@ -74,7 +74,7 @@ impl State { State { transitions, action: None, - counter: None, + counter: (None, None), } } @@ -99,7 +99,7 @@ impl State { let mut seen: HashSet = HashSet::new(); for t in transitions.iter() { - if t.0 >= num_states && t.0 != STATE_END { + if t.0 >= num_states && t.0 != STATE_END && t.0 != STATE_SIGNAL { Err(Error::Machine(format!( "found out-of-bounds state index {}", t.0 @@ -135,7 +135,10 @@ impl State { if let Some(action) = &self.action { action.validate()?; } - if let Some(counter) = &self.counter { + if let Some(counter) = &self.counter.0 { + counter.validate()?; + } + if let Some(counter) = &self.counter.1 { counter.validate()?; } @@ -182,11 +185,21 @@ impl fmt::Display for State { } else { writeln!(f, "action: None")?; } - if let Some(counter) = self.counter { - writeln!(f, "counter: {}", counter)?; - } else { - writeln!(f, "counter: None")?; - } + match self.counter { + (Some(counter_a), Some(counter_b)) => { + writeln!(f, "counter A: {}", counter_a)?; + writeln!(f, "counter B: {}", counter_b)?; + } + (Some(counter), None) => { + writeln!(f, "counter A: {}", counter)?; + } + (None, Some(counter)) => { + writeln!(f, "counter B: {}", counter)?; + } + _ => { + writeln!(f, "counter: None")?; + } + }; writeln!(f, "transitions: ")?; for event in Event::iter() { @@ -200,7 +213,6 @@ impl fmt::Display for State { if trans != vector.last().unwrap() { write!(f, ",")?; } - } writeln!(f)?; } @@ -212,7 +224,7 @@ impl fmt::Display for State { #[cfg(test)] mod tests { - use crate::counter::{Counter, CounterUpdate, Operation}; + use crate::counter::{Counter, Operation}; use crate::dist::{Dist, DistType}; use crate::event::Event; use crate::state::*; @@ -357,29 +369,27 @@ mod tests { Event::PaddingSent => vec![Trans(0, 1.0)], _ => vec![], }); - s.counter = Some(CounterUpdate { - counter: Counter::A, - operation: Operation::Increment, - value: None, - }); + s.counter = (Some(Counter::new(Operation::Increment)), None); let r = s.validate(num_states); println!("{:?}", r.as_ref().err()); assert!(r.is_ok()); // invalid counter update in state - s.counter = Some(CounterUpdate { - counter: Counter::B, - operation: Operation::Set, - value: Some(Dist { - dist: DistType::Uniform { - low: 2.0, // NOTE low > high - high: 1.0, + s.counter = ( + None, + Some(Counter::new_dist( + Operation::Increment, + Dist { + dist: DistType::Uniform { + low: 2.0, // NOTE low > high + high: 1.0, + }, + start: 0.0, + max: 0.0, }, - start: 0.0, - max: 0.0, - }), - }); + )), + ); let r = s.validate(num_states); println!("{:?}", r.as_ref().err());