use std::borrow::Cow;
use std::collections::BTreeSet;
use std::hash::Hash;
use num_bigint::BigUint;
use stateright::actor::model_timeout;
use stateright::actor::Actor;
use stateright::actor::ActorModel;
use stateright::actor::ActorModelState;
use stateright::actor::Id;
use stateright::actor::Network;
use stateright::actor::Out;
use stateright::Checker;
use stateright::Expectation;
use stateright::Model;
use super::test_dht_convergence::spec;
use super::test_dht_convergence::K;
use crate::dht::successor::SuccessorReader;
use crate::dht::successor::SuccessorWriter;
use crate::dht::Chord;
use crate::dht::Did;
use crate::dht::PeerRing;
use crate::storage::MemStorage;
fn did_frac(num: u64, den: u64) -> Did {
Did::from((BigUint::from(1u8) << 160) * BigUint::from(num) / BigUint::from(den))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(super) struct DhtSnapshot {
pub did: Did,
pub succ: Vec<Did>,
pub pred: Option<Did>,
pub finger: Vec<Option<Did>>,
}
impl DhtSnapshot {
pub(super) fn capture(dht: &PeerRing) -> Self {
Self {
did: dht.did,
succ: dht.successors().list().unwrap(),
pred: *dht.lock_predecessor().unwrap(),
finger: dht.lock_finger().unwrap().list().clone(),
}
}
pub(super) fn restore(&self) -> PeerRing {
let dht = PeerRing::new_with_storage(self.did, K as u8, Box::new(MemStorage::new()));
for &s in &self.succ {
dht.successors().update(s).unwrap();
}
*dht.lock_predecessor().unwrap() = self.pred;
{
let mut finger = dht.lock_finger().unwrap();
for (i, entry) in self.finger.iter().enumerate() {
if let Some(d) = entry {
finger.set(i, *d);
}
}
}
dht
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum Msg {
NotifyPred { from: Did },
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum Timer {
Stabilize,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct NodeState {
pred: Option<Did>,
}
#[derive(Clone)]
struct ChordNode {
all: Vec<Did>,
}
impl ChordNode {
fn did(&self, id: Id) -> Did {
self.all[usize::from(id)]
}
fn id_of(&self, did: Did) -> Id {
Id::from(
self.all
.iter()
.position(|&d| d == did)
.expect("did belongs to the modelled set"),
)
}
fn apply_notify(&self, me: Did, current: Option<Did>, from: Did) -> Did {
match current {
Some(cur) if spec::dist(me, cur) >= spec::dist(me, from) => cur,
_ => from,
}
}
}
impl Actor for ChordNode {
type Msg = Msg;
type State = NodeState;
type Timer = Timer;
type Random = ();
type Storage = ();
fn on_start(&self, _id: Id, _storage: &Option<()>, o: &mut Out<Self>) -> NodeState {
o.set_timer(Timer::Stabilize, model_timeout());
NodeState { pred: None }
}
fn on_timeout(&self, id: Id, _state: &mut Cow<NodeState>, _timer: &Timer, o: &mut Out<Self>) {
let me = self.did(id);
for s in spec::successors(&self.all, me) {
o.send(self.id_of(s), Msg::NotifyPred { from: me });
}
o.set_timer(Timer::Stabilize, model_timeout());
}
fn on_msg(&self, id: Id, state: &mut Cow<NodeState>, _src: Id, msg: Msg, _o: &mut Out<Self>) {
let me = self.did(id);
match msg {
Msg::NotifyPred { from } => {
let new_pred = self.apply_notify(me, state.pred, from);
if state.pred != Some(new_pred) {
state.to_mut().pred = Some(new_pred);
}
}
}
}
}
#[derive(Clone)]
struct Cfg {
all: Vec<Did>,
}
fn prop_pred_wellformed(
model: &ActorModel<ChordNode, Cfg, ()>,
st: &ActorModelState<ChordNode>,
) -> bool {
st.actor_states
.iter()
.enumerate()
.all(|(i, s)| match s.pred {
None => true,
Some(p) => p != model.cfg.all[i] && model.cfg.all.contains(&p),
})
}
fn prop_all_converged(
model: &ActorModel<ChordNode, Cfg, ()>,
st: &ActorModelState<ChordNode>,
) -> bool {
st.actor_states
.iter()
.enumerate()
.all(|(i, s)| s.pred == spec::predecessor(&model.cfg.all, model.cfg.all[i]))
}
fn notify_model(all: Vec<Did>) -> ActorModel<ChordNode, Cfg, ()> {
let actors: Vec<ChordNode> = all.iter().map(|_| ChordNode { all: all.clone() }).collect();
ActorModel::new(Cfg { all }, ())
.actors(actors)
.init_network(Network::new_unordered_duplicating([]))
.property(
Expectation::Always,
"predecessor well-formed",
prop_pred_wellformed,
)
.property(
Expectation::Sometimes,
"convergence reachable",
prop_all_converged,
)
.property(
Expectation::Eventually,
"convergence inevitable",
prop_all_converged,
)
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum DMsg {
Lookup { origin: usize },
Found { node: usize },
NotifyPred { from: usize },
NotifyPredReport { pred: usize },
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum DTimer {
Stabilize,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct DState {
connected: BTreeSet<usize>,
pred: Option<usize>,
ticks: u8,
}
#[derive(Clone)]
struct DiscoveryNode {
all: Vec<Did>,
rounds: u8,
}
impl DiscoveryNode {
fn successors(&self, me: usize, connected: &BTreeSet<usize>) -> Vec<usize> {
let mut v: Vec<usize> = connected.iter().copied().filter(|&c| c != me).collect();
v.sort_by_key(|&c| spec::dist(self.all[me], self.all[c]));
v.truncate(K);
v
}
fn successor_of(&self, me: usize, connected: &BTreeSet<usize>, target: Did) -> usize {
std::iter::once(me)
.chain(connected.iter().copied())
.min_by_key(|&n| spec::dist(target, self.all[n]))
.unwrap()
}
fn notify(&self, me: usize, cur: Option<usize>, from: usize) -> usize {
match cur {
Some(p)
if spec::dist(self.all[me], self.all[p])
>= spec::dist(self.all[me], self.all[from]) =>
{
p
}
_ => from,
}
}
}
impl Actor for DiscoveryNode {
type Msg = DMsg;
type State = DState;
type Timer = DTimer;
type Random = ();
type Storage = ();
fn on_start(&self, id: Id, _storage: &Option<()>, o: &mut Out<Self>) -> DState {
let me = usize::from(id);
let connected = if me == 0 {
BTreeSet::new()
} else {
o.send(Id::from(0usize), DMsg::Lookup { origin: me });
BTreeSet::from([0])
};
o.set_timer(DTimer::Stabilize, model_timeout());
DState {
connected,
pred: None,
ticks: 0,
}
}
fn on_timeout(&self, id: Id, state: &mut Cow<DState>, _t: &DTimer, o: &mut Out<Self>) {
if state.ticks >= self.rounds {
return;
}
let me = usize::from(id);
for s in self.successors(me, &state.connected) {
if s != me {
o.send(Id::from(s), DMsg::NotifyPred { from: me });
}
}
state.to_mut().ticks += 1;
o.set_timer(DTimer::Stabilize, model_timeout());
}
fn on_msg(&self, id: Id, state: &mut Cow<DState>, _src: Id, msg: DMsg, o: &mut Out<Self>) {
let me = usize::from(id);
match msg {
DMsg::Lookup { origin } => {
let succ = self.successor_of(me, &state.connected, self.all[origin]);
o.send(Id::from(origin), DMsg::Found { node: succ });
if origin != me && !state.connected.contains(&origin) {
state.to_mut().connected.insert(origin);
}
}
DMsg::Found { node } => {
if node != me && !state.connected.contains(&node) {
state.to_mut().connected.insert(node);
o.send(Id::from(node), DMsg::Lookup { origin: me });
}
}
DMsg::NotifyPred { from } => {
let new_pred = self.notify(me, state.pred, from);
if state.pred != Some(new_pred) {
state.to_mut().pred = Some(new_pred);
}
if new_pred != from {
o.send(Id::from(from), DMsg::NotifyPredReport { pred: new_pred });
}
}
DMsg::NotifyPredReport { pred } => {
if pred != me && !state.connected.contains(&pred) {
state.to_mut().connected.insert(pred);
o.send(Id::from(pred), DMsg::Lookup { origin: me });
}
}
}
}
}
fn succ_among(all: &[Did], me: Did, connected: &BTreeSet<usize>) -> Vec<Did> {
let mut v: Vec<Did> = connected
.iter()
.map(|&i| all[i])
.filter(|&d| d != me)
.collect();
v.sort_by_key(|&d| spec::dist(me, d));
v.truncate(K);
v
}
fn d_wellformed(
model: &ActorModel<DiscoveryNode, Cfg, ()>,
st: &ActorModelState<DiscoveryNode>,
) -> bool {
let all = &model.cfg.all;
st.actor_states.iter().enumerate().all(|(i, s)| {
s.connected.iter().all(|&c| c < all.len() && c != i)
&& s.pred.is_none_or(|p| p < all.len() && p != i)
})
}
fn d_converged(
model: &ActorModel<DiscoveryNode, Cfg, ()>,
st: &ActorModelState<DiscoveryNode>,
) -> bool {
let all = &model.cfg.all;
(0..all.len()).all(|i| {
let s = &st.actor_states[i];
succ_among(all, all[i], &s.connected) == spec::successors(all, all[i])
&& s.pred.map(|p| all[p]) == spec::predecessor(all, all[i])
})
}
fn discovery_model(all: Vec<Did>, rounds: u8) -> ActorModel<DiscoveryNode, Cfg, ()> {
let actors: Vec<DiscoveryNode> = all
.iter()
.map(|_| DiscoveryNode {
all: all.clone(),
rounds,
})
.collect();
ActorModel::new(Cfg { all }, ())
.actors(actors)
.init_network(Network::new_unordered_nonduplicating([]))
.property(
Expectation::Always,
"connected/pred well-formed",
d_wellformed,
)
.property(
Expectation::Sometimes,
"discovery converges (reachable)",
d_converged,
)
}
mod storage_model;
#[cfg(test)]
mod tests {
use super::storage_model::*;
use super::*;
fn build_converged(node: Did, all: &[Did]) -> PeerRing {
let dht = PeerRing::new_with_storage(node, K as u8, Box::new(MemStorage::new()));
for &other in all {
if other != node {
dht.join(other).unwrap();
dht.notify(other).unwrap();
}
}
dht
}
#[test]
fn test_apply_notify_matches_peer_ring() {
let all: Vec<Did> = (0..4u64).map(|i| did_frac(i, 4)).collect();
let node = ChordNode { all: all.clone() };
let candidates = std::iter::once(None).chain(all.iter().copied().map(Some));
for &me in &all {
for current in candidates.clone() {
if current == Some(me) {
continue;
}
for &from in all.iter().filter(|&&d| d != me) {
let dht = PeerRing::new_with_storage(me, K as u8, Box::new(MemStorage::new()));
*dht.lock_predecessor().unwrap() = current;
let real = dht.notify(from).unwrap();
assert_eq!(
node.apply_notify(me, current, from),
real,
"apply_notify != PeerRing::notify (me={me}, cur={current:?}, from={from})"
);
}
}
}
}
#[test]
fn test_successors_match_peer_ring_on_partial_states() {
let all: Vec<Did> = (0..4u64).map(|i| did_frac(i, 4)).collect();
let node = DiscoveryNode {
all: all.clone(),
rounds: 1,
};
let n = all.len();
for me in 0..n {
let others: Vec<usize> = (0..n).filter(|&i| i != me).collect();
for mask in 0u32..(1 << others.len()) {
let connected: BTreeSet<usize> = others
.iter()
.enumerate()
.filter(|(bit, _)| mask & (1 << bit) != 0)
.map(|(_, &i)| i)
.collect();
let model: Vec<Did> = node
.successors(me, &connected)
.into_iter()
.map(|i| all[i])
.collect();
let dht = PeerRing::new_with_storage(all[me], K as u8, Box::new(MemStorage::new()));
for &c in &connected {
let _ = dht.join(all[c]);
}
let real = dht.successors().list().unwrap();
assert_eq!(
model, real,
"successors disagree with PeerRing (me={me}, connected={connected:?})"
);
}
}
}
#[test]
fn test_snapshot_round_trip_is_lossless() {
for n in 2..=6u64 {
let dids: Vec<Did> = (0..n).map(|i| did_frac(i, n)).collect();
for &node in &dids {
let original = DhtSnapshot::capture(&build_converged(node, &dids));
let restored = DhtSnapshot::capture(&original.restore());
pretty_assertions::assert_eq!(restored, original, "round-trip lossy at {node}");
}
}
}
#[test]
fn test_notify_predecessor_converges_under_full_mesh() {
let all: Vec<Did> = (0..3u64).map(|i| did_frac(i, 3)).collect();
notify_model(all)
.checker()
.spawn_bfs()
.join()
.assert_properties();
}
#[test]
fn test_discovery_is_safe_and_can_converge() {
let all: Vec<Did> = (0..3u64).map(|i| did_frac(i, 3)).collect();
discovery_model(all, 2)
.checker()
.spawn_bfs()
.join()
.assert_properties();
}
#[test]
fn test_discovery_has_no_bounded_convergence() {
for rounds in [1u8, 2] {
let all: Vec<Did> = (0..3u64).map(|i| did_frac(i, 3)).collect();
let actors: Vec<DiscoveryNode> = all
.iter()
.map(|_| DiscoveryNode {
all: all.clone(),
rounds,
})
.collect();
let checker = ActorModel::new(Cfg { all }, ())
.actors(actors)
.init_network(Network::new_unordered_nonduplicating([]))
.property(Expectation::Eventually, "bounded convergence", d_converged)
.checker()
.spawn_bfs()
.join();
assert!(
checker.discovery("bounded convergence").is_some(),
"expected a no-bounded-convergence counterexample at rounds={rounds} \
(a peer learned after a node's stabilization budget is never notified)"
);
}
}
#[test]
fn test_storage_entry_join_satisfies_semilattice_laws() {
for carrier in storage_join_carriers() {
let values = (0u8..8)
.map(|bits| storage_value_from_bits(carrier, bits))
.collect::<Vec<_>>();
for left in &values {
let idempotent = storage_join_entry(left.entry.clone(), left.entry.clone());
assert_eq!(idempotent, left.entry);
for right in &values {
let expected = storage_value_by_bits(&values, left.bits | right.bits);
let left_right = storage_join_entry(left.entry.clone(), right.entry.clone());
let right_left = storage_join_entry(right.entry.clone(), left.entry.clone());
assert_eq!(left_right, expected.entry);
assert_eq!(left_right, right_left);
for third in &values {
let left_assoc = storage_join_entry(
storage_join_entry(left.entry.clone(), right.entry.clone()),
third.entry.clone(),
);
let right_assoc = storage_join_entry(
left.entry.clone(),
storage_join_entry(right.entry.clone(), third.entry.clone()),
);
assert_eq!(left_assoc, right_assoc);
}
}
}
}
}
#[test]
fn test_storage_join_topology_model_converges_after_partition_merge() {
for scenario in storage_join_scenarios() {
let global_lub = scenario.global_lub();
for partition in STORAGE_PARTITION_MASKS {
for state in reachable_storage_join_states(partition, scenario.initial.clone()) {
if state.phase == StorageJoinPhase::Merged {
let closed = state.drive_to_quiescent_lub(&global_lub);
assert!(
closed.is_quiescent_lub(&global_lub),
"merged state did not close to global lub for {}: start={state:?}, closed={closed:?}",
scenario.name
);
}
}
}
}
}
#[test]
fn test_storage_sync_model_preserves_no_update_loss() {
let mut seen = BTreeSet::new();
let mut frontier = vec![StorageSyncState::initial()];
while let Some(state) = frontier.pop() {
if !seen.insert(state) {
continue;
}
let steps = [
StorageSyncStep::SendCopy,
StorageSyncStep::DeliverCopy,
StorageSyncStep::LocalWrite(StorageValue::V0),
StorageSyncStep::LocalWrite(StorageValue::V1),
StorageSyncStep::LocalWrite(StorageValue::V2),
StorageSyncStep::DeliverAckDelete,
];
for step in steps {
if let Some(next) = state.step(step) {
if let Some(removed) = state.removed_local_value(next) {
assert_eq!(
next.successor,
Some(removed),
"S2' violated by {step:?}: {state:?} -> {next:?}"
);
}
if !seen.contains(&next) {
frontier.push(next);
}
}
}
}
}
}