aleph_bft/
config.rs

1use crate::{NodeCount, NodeIndex, Round, SessionId};
2use log::error;
3use std::{
4    fmt::{Debug, Formatter},
5    sync::Arc,
6    time::Duration,
7};
8
9#[derive(Debug)]
10pub struct InvalidConfigError;
11
12/// A function answering the question of how long to delay the n-th retry.
13pub type DelaySchedule = Arc<dyn Fn(usize) -> Duration + Sync + Send + 'static>;
14
15/// A function answering the question of how many nodes to query on the n-th (0-based) try.
16pub type RecipientCountSchedule = Arc<dyn Fn(usize) -> usize + Sync + Send + 'static>;
17
18/// Configuration of several parameters related to delaying various tasks.
19#[derive(Clone)]
20pub struct DelayConfig {
21    /// Tick frequency of the request task queue, impacts how often tasks are send out.
22    pub tick_interval: Duration,
23    /// Minimum frequency of broadcast of top known units. Units have to be at least this old to be
24    /// rebroadcast at all.
25    pub unit_rebroadcast_interval_min: Duration,
26    /// Maximum frequency of broadcast of top known units.
27    pub unit_rebroadcast_interval_max: Duration,
28    /// unit_creation_delay(k) represents the delay between creating the (k-1)th and kth unit.
29    pub unit_creation_delay: DelaySchedule,
30    /// coord_request_delay(k) represents the delay between the kth and (k+1)st try when requesting
31    /// a unit by coords.
32    pub coord_request_delay: DelaySchedule,
33    /// coord_request_recipients(k) represents the number of nodes to ask at the kth try when
34    /// requesting a unit by coords.
35    pub coord_request_recipients: RecipientCountSchedule,
36    /// parent_request_delay(k) represents the delay between the kth and (k+1)st try when requesting
37    /// unknown parents of a unit.
38    pub parent_request_delay: DelaySchedule,
39    /// parent_request_recipients(k) represents the number of nodes to ask at the kth try when
40    /// requesting unknown parents of a unit.
41    pub parent_request_recipients: RecipientCountSchedule,
42    /// newest_request_delay(k) represents the delay between the kth and (k+1)st try when sending
43    /// a broadcast request for newest units
44    pub newest_request_delay: DelaySchedule,
45}
46
47impl Debug for DelayConfig {
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("DelayConfig")
50            .field("tick interval", &self.tick_interval)
51            .field(
52                "min unit rebroadcast interval",
53                &self.unit_rebroadcast_interval_min,
54            )
55            .field(
56                "max unit rebroadcast interval",
57                &self.unit_rebroadcast_interval_max,
58            )
59            .finish()
60    }
61}
62
63/// Main configuration of the consensus. We refer to [the documentation](https://cardinal-cryptography.github.io/AlephBFT/aleph_bft_api.html#34-alephbft-sessions)
64/// Section 3.4 for a discussion of some of these parameters and their significance.
65#[derive(Clone, Debug)]
66pub struct Config {
67    /// Identification number of the Member=0,..,(n_members-1).
68    node_ix: NodeIndex,
69    /// Id of the session for which this instance is run.
70    session_id: SessionId,
71    /// The size of the committee running the consensus.
72    n_members: NodeCount,
73    /// Configuration of several parameters related to delaying various tasks.
74    delay_config: DelayConfig,
75    /// Maximum allowable round of a unit.
76    max_round: Round,
77}
78
79impl Config {
80    pub fn node_ix(&self) -> NodeIndex {
81        self.node_ix
82    }
83    pub fn session_id(&self) -> SessionId {
84        self.session_id
85    }
86    pub fn n_members(&self) -> NodeCount {
87        self.n_members
88    }
89    pub fn delay_config(&self) -> &DelayConfig {
90        &self.delay_config
91    }
92    pub fn max_round(&self) -> Round {
93        self.max_round
94    }
95}
96
97pub fn exponential_slowdown(
98    t: usize,
99    base_delay: f64,
100    start_exp_delay: usize,
101    exp_base: f64,
102) -> Duration {
103    // This gives:
104    // base_delay, for t <= start_exp_delay,
105    // base_delay * exp_base^(t - start_exp_delay), for t > start_exp_delay.
106    let delay = if t < start_exp_delay {
107        base_delay
108    } else {
109        let power = t - start_exp_delay;
110        base_delay * exp_base.powf(power as f64)
111    };
112    let delay = delay.round() as u64;
113    // the above will make it u64::MAX if it exceeds u64
114    Duration::from_millis(delay)
115}
116
117/// Creates a [`Config`] which wraps the passed arguments. `time_to_reach_max_round` is a lower bound
118/// on the time needed to reach the maximum round expected by the user and is only used for verification.
119pub fn create_config(
120    n_members: NodeCount,
121    node_ix: NodeIndex,
122    session_id: SessionId,
123    max_round: Round,
124    delay_config: DelayConfig,
125    time_to_reach_max_round: Duration,
126) -> Result<Config, InvalidConfigError> {
127    if time_to_reach_round(max_round, &delay_config.unit_creation_delay) < time_to_reach_max_round {
128        error!(
129            target: "AlephBFT-config",
130            "Reaching max_round will happen too fast with the given Config. Consider increasing max_round or lowering time_to_reach_max_round."
131        );
132        return Err(InvalidConfigError);
133    }
134
135    Ok(Config {
136        node_ix,
137        session_id,
138        n_members,
139        delay_config,
140        max_round,
141    })
142}
143
144/// Creates a [`Config`], allowing the user to omit specifying the `delay_config` in which case it will be
145/// set to default, suggested by the creators of this package. `time_to_reach_max_round` is a lower bound
146/// on the time needed to reach the maximum round expected by the user and is only used for verification.
147pub fn default_config(
148    n_members: NodeCount,
149    node_ix: NodeIndex,
150    session_id: SessionId,
151    max_round: Round,
152    time_to_reach_max_round: Duration,
153) -> Result<Config, InvalidConfigError> {
154    let delay_config = default_delay_config();
155    create_config(
156        n_members,
157        node_ix,
158        session_id,
159        max_round,
160        delay_config,
161        time_to_reach_max_round,
162    )
163}
164
165/// Creates a [`DelayConfig`] with default parameters, suggested by the creators of this package.
166pub fn default_delay_config() -> DelayConfig {
167    DelayConfig {
168        tick_interval: Duration::from_millis(20),
169        unit_rebroadcast_interval_min: Duration::from_millis(15000),
170        unit_rebroadcast_interval_max: Duration::from_millis(20000),
171        unit_creation_delay: default_unit_creation_delay(),
172        coord_request_delay: default_coord_request_delay(),
173        coord_request_recipients: default_coord_request_recipients(),
174        parent_request_delay: Arc::new(|_| Duration::from_millis(3000)),
175        parent_request_recipients: Arc::new(|_| 1),
176        newest_request_delay: Arc::new(|_| Duration::from_millis(3000)),
177    }
178}
179
180/// 5000, 500, 500, 500, ... (till step 3000), 500, 500*1.005, 500*(1.005)^2, 500*(1.005)^3, ..., 10742207 (last step)
181fn default_unit_creation_delay() -> DelaySchedule {
182    Arc::new(|t| match t {
183        0 => Duration::from_millis(5000),
184        _ => exponential_slowdown(t, 500.0, 3000, 1.005),
185    })
186}
187
188/// 200, 1000, 3000, 6000, 9000, ...
189/// Note that the first request always gets send immediately, so these are delays for _after_ a
190/// request is sent for the nth time.
191fn default_coord_request_delay() -> DelaySchedule {
192    Arc::new(|t| match t {
193        0 => Duration::from_millis(200),
194        1 => Duration::from_millis(1000),
195        _ => Duration::from_millis(3000 * (t as u64 - 1)),
196    })
197}
198
199/// 3, 3, 3, 1, 1, 1, 1, ...
200fn default_coord_request_recipients() -> RecipientCountSchedule {
201    Arc::new(|t| if t <= 2 { 3 } else { 1 })
202}
203
204fn time_to_reach_round(round: Round, delay_schedule: &DelaySchedule) -> Duration {
205    let mut total_time = Duration::from_millis(0);
206    for r in 0..round {
207        total_time += delay_schedule(r as usize);
208    }
209    total_time
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::{
215        config::{
216            default_coord_request_delay, default_coord_request_recipients, time_to_reach_round,
217            DelaySchedule,
218        },
219        create_config, exponential_slowdown, DelayConfig, NodeCount, NodeIndex,
220    };
221    use std::{sync::Arc, time::Duration};
222
223    const MILLIS_IN_WEEK: u64 = 1000 * 60 * 60 * 24 * 7;
224
225    fn delay_config_for_tests() -> DelayConfig {
226        DelayConfig {
227            tick_interval: Duration::from_millis(10),
228            unit_rebroadcast_interval_min: Duration::from_millis(15000),
229            unit_rebroadcast_interval_max: Duration::from_millis(20000),
230            unit_creation_delay: Arc::new(move |t| match t {
231                0 => Duration::from_millis(2000),
232                _ => exponential_slowdown(t, 300.0, 5000, 1.005),
233            }),
234            coord_request_delay: default_coord_request_delay(),
235            coord_request_recipients: default_coord_request_recipients(),
236            parent_request_delay: Arc::new(|_| Duration::from_millis(3000)),
237            parent_request_recipients: Arc::new(|_| 1),
238            newest_request_delay: Arc::new(|_| Duration::from_millis(3000)),
239        }
240    }
241
242    #[test]
243    fn time_to_reach_delay_is_correct() {
244        let delay_schedule: DelaySchedule = Arc::new(|r| {
245            Duration::from_millis(match r {
246                0 => 2,
247                1 => 3,
248                2 => 5,
249                3 => 7,
250                4 => 11,
251                _ => 13,
252            })
253        });
254
255        assert_eq!(
256            time_to_reach_round(0, &delay_schedule),
257            Duration::from_millis(0)
258        );
259        assert_eq!(
260            time_to_reach_round(1, &delay_schedule),
261            Duration::from_millis(2)
262        );
263        assert_eq!(
264            time_to_reach_round(5, &delay_schedule),
265            Duration::from_millis(28)
266        );
267        assert_eq!(
268            time_to_reach_round(10, &delay_schedule),
269            Duration::from_millis(93)
270        );
271    }
272
273    #[test]
274    fn low_round_not_causing_slowdown_fails_the_check() {
275        let config = create_config(
276            NodeCount(5),
277            NodeIndex(1),
278            3,
279            5000,
280            delay_config_for_tests(),
281            Duration::from_millis(MILLIS_IN_WEEK),
282        );
283
284        assert!(config.is_err());
285    }
286
287    #[test]
288    fn high_round_causing_slowdown_passes_the_check() {
289        let config = create_config(
290            NodeCount(5),
291            NodeIndex(1),
292            3,
293            7000,
294            delay_config_for_tests(),
295            Duration::from_millis(MILLIS_IN_WEEK),
296        );
297
298        assert!(config.is_ok());
299    }
300}