fedimint_server/consensus/
peer_availability.rs1use std::collections::{BTreeMap, BTreeSet};
17use std::io::{Error, Read, Write};
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
22use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
23use fedimint_core::envs::is_running_in_test_env;
24use fedimint_core::module::registry::ModuleDecoderRegistry;
25use fedimint_core::peer_availability::{
26 PEER_AVAILABILITY_WINDOWS_SECS, PeerAvailability, WindowAvailability,
27};
28use fedimint_core::task::sleep;
29use fedimint_core::time::duration_since_epoch;
30use fedimint_core::{NumPeers, PeerId};
31use fedimint_logging::LOG_CONSENSUS;
32use tracing::debug;
33
34use super::db::PeerAvailabilityKey;
35
36const SLOT_SECS: u64 = 60;
37
38pub async fn persist_peer_availability(db: Database, peer_activity: Arc<Mutex<PeerActivity>>) {
42 let interval = if is_running_in_test_env() {
43 Duration::from_secs(10)
44 } else {
45 Duration::from_secs(600)
46 };
47
48 loop {
49 sleep(interval).await;
50
51 let now_secs = duration_since_epoch().as_secs();
52 let activity = peer_activity.lock().expect("poisoned").take(now_secs);
53
54 debug!(target: LOG_CONSENSUS, ?activity, "Peer activity since last update");
55
56 let mut dbtx = db.begin_transaction().await;
57 let mut state = dbtx
58 .get_value(&PeerAvailabilityKey)
59 .await
60 .unwrap_or_default();
61 state.update(now_secs, &activity);
62 dbtx.insert_entry(&PeerAvailabilityKey, &state).await;
63 dbtx.commit_tx().await;
64 }
65}
66
67#[derive(Debug)]
69pub struct PeerActivity {
70 identity: PeerId,
71 num_peers: NumPeers,
72 slot: u64,
73 heard_in_slot: BTreeSet<PeerId>,
74 observed_slots: u32,
75 active_slots: BTreeMap<PeerId, u32>,
76 last_heard_secs: BTreeMap<PeerId, u64>,
77}
78
79#[derive(Debug, Clone, Default, PartialEq)]
81pub struct ObservedActivity {
82 pub observed_secs: u64,
83 pub availability: BTreeMap<PeerId, f64>,
85 pub last_heard_secs: BTreeMap<PeerId, u64>,
86}
87
88impl PeerActivity {
89 pub fn new(identity: PeerId, num_peers: NumPeers) -> Self {
90 Self {
91 identity,
92 num_peers,
93 slot: 0,
94 heard_in_slot: BTreeSet::new(),
95 observed_slots: 0,
96 active_slots: BTreeMap::new(),
97 last_heard_secs: BTreeMap::new(),
98 }
99 }
100
101 pub fn heard(&mut self, peer: PeerId, now_secs: u64) {
103 self.advance(now_secs);
104 self.heard_in_slot.insert(peer);
105 self.last_heard_secs.insert(peer, now_secs);
106 }
107
108 pub fn take(&mut self, now_secs: u64) -> ObservedActivity {
110 self.advance(now_secs);
111
112 let observed_slots = std::mem::take(&mut self.observed_slots);
113 let mut active_slots = std::mem::take(&mut self.active_slots);
114
115 active_slots.insert(self.identity, observed_slots);
117 if 0 < observed_slots {
118 self.last_heard_secs.insert(self.identity, now_secs);
119 }
120
121 ObservedActivity {
122 observed_secs: u64::from(observed_slots) * SLOT_SECS,
123 availability: self
124 .num_peers
125 .peer_ids()
126 .filter(|_| 0 < observed_slots)
127 .map(|peer| {
128 let active = active_slots.get(&peer).copied().unwrap_or(0);
129 (peer, f64::from(active) / f64::from(observed_slots))
130 })
131 .collect(),
132 last_heard_secs: self.last_heard_secs.clone(),
133 }
134 }
135
136 fn advance(&mut self, now_secs: u64) {
139 let slot = now_secs / SLOT_SECS;
140 if slot == self.slot {
141 return;
142 }
143
144 if self.num_peers.threshold() - 1 <= self.heard_in_slot.len() {
146 self.observed_slots += 1;
147 for peer in &self.heard_in_slot {
148 *self.active_slots.entry(*peer).or_default() += 1;
149 }
150 }
151
152 self.heard_in_slot.clear();
153 self.slot = slot;
154 }
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Encodable, Decodable)]
159pub struct PeerAvailabilityState {
160 updated_at_secs: u64,
161 last_seen_secs: BTreeMap<PeerId, u64>,
163 coverage: Vec<Average>,
165 availability: Vec<BTreeMap<PeerId, Average>>,
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq)]
171struct Average(f64);
172
173impl Encodable for Average {
174 fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
175 self.0.to_bits().consensus_encode(writer)
176 }
177}
178
179impl Decodable for Average {
180 fn consensus_decode_partial<R: Read>(
181 reader: &mut R,
182 modules: &ModuleDecoderRegistry,
183 ) -> Result<Self, DecodeError> {
184 Ok(Self(f64::from_bits(u64::consensus_decode_partial(
185 reader, modules,
186 )?)))
187 }
188}
189
190impl PeerAvailabilityState {
191 pub fn update(&mut self, now_secs: u64, activity: &ObservedActivity) {
193 for (peer, heard) in &activity.last_heard_secs {
194 let last_seen = self.last_seen_secs.entry(*peer).or_default();
195 *last_seen = (*last_seen).max(*heard);
196 }
197
198 let elapsed = now_secs.saturating_sub(self.updated_at_secs) as f64;
201 let observed = activity.observed_secs as f64;
202
203 let windows = PEER_AVAILABILITY_WINDOWS_SECS.len();
204 self.coverage.resize(windows, Average::default());
205 self.availability.resize(windows, BTreeMap::new());
206
207 for (window, window_secs) in PEER_AVAILABILITY_WINDOWS_SECS.iter().enumerate() {
208 let decay = (-elapsed.max(observed) / *window_secs as f64).exp();
209 let gain = 1.0 - (-observed / *window_secs as f64).exp();
210
211 self.coverage[window].0 = self.coverage[window].0 * decay + gain;
212
213 for average in self.availability[window].values_mut() {
214 average.0 *= decay;
215 }
216
217 for (peer, value) in &activity.availability {
218 self.availability[window].entry(*peer).or_default().0 += gain * value;
219 }
220 }
221
222 self.updated_at_secs = now_secs;
223 }
224
225 pub fn report(&self, now_secs: u64) -> PeerAvailability {
227 let elapsed = now_secs.saturating_sub(self.updated_at_secs) as f64;
228
229 PeerAvailability {
230 last_seen_secs: self.last_seen_secs.clone(),
231 windows: PEER_AVAILABILITY_WINDOWS_SECS
232 .iter()
233 .zip(&self.coverage)
234 .zip(&self.availability)
235 .map(
236 |((window_secs, coverage), availability)| WindowAvailability {
237 window_secs: *window_secs,
238 coverage_bps: to_bps(coverage.0 * (-elapsed / *window_secs as f64).exp()),
241 peers: availability
242 .iter()
243 .map(|(peer, average)| (*peer, to_bps(average.0 / coverage.0)))
244 .collect(),
245 },
246 )
247 .collect(),
248 }
249 }
250}
251
252fn to_bps(value: f64) -> u16 {
253 (value * 10_000.0).round().clamp(0.0, 10_000.0) as u16
254}
255
256#[cfg(test)]
257mod tests;