cuprate_dandelion_tower/config.rs
1use std::{
2 ops::{Mul, Neg},
3 time::Duration,
4};
5
6/// When calculating the embargo timeout using the formula: `(-k*(k-1)*hop)/(2*log(1-ep))`
7///
8/// (1 - ep) is the probability that a transaction travels for `k` hops before a nodes embargo timeout fires, this constant is (1 - ep).
9const EMBARGO_FULL_TRAVEL_PROBABILITY: f64 = 0.90;
10
11/// The graph type to use for dandelion routing, the dandelion paper recommends [`Graph::FourRegular`].
12///
13/// The decision between line graphs and 4-regular graphs depend on the priorities of the system, if
14/// linkability of transactions is a first order concern then line graphs may be better, however 4-regular graphs
15/// can give constant-order privacy benefits against adversaries with knowledge of the graph.
16///
17/// See appendix C of the dandelion++ paper.
18#[derive(Default, Debug, Copy, Clone)]
19pub enum Graph {
20 /// Line graph.
21 ///
22 /// When this is selected one peer will be chosen from the outbound peers each epoch to route transactions
23 /// to.
24 ///
25 /// In general this is not recommend over [`Graph::FourRegular`] but may be better for certain systems.
26 Line,
27 /// Quasi-4-Regular.
28 ///
29 /// When this is selected two peers will be chosen from the outbound peers each epoch, each stem transaction
30 /// received will then be sent to one of these two peers. Transactions from the same node will always go to the
31 /// same peer.
32 #[default]
33 FourRegular,
34}
35
36/// The config used to initialize dandelion.
37///
38/// One notable missing item from the config is `Tbase` AKA the timeout parameter to prevent black hole
39/// attacks. This is removed from the config for simplicity, `Tbase` is calculated using the formula provided
40/// in the D++ paper:
41///
42/// `(-k*(k-1)*hop)/(2*log(1-ep))`
43///
44/// Where `k` is calculated from the fluff probability, `hop` is `time_between_hop` and `ep` is fixed at `0.1`.
45#[derive(Debug, Clone, Copy)]
46pub struct DandelionConfig {
47 /// The time it takes for a stem transaction to pass through a node, including network latency.
48 ///
49 /// It's better to be safe and put a slightly higher value than lower.
50 pub time_between_hop: Duration,
51 /// The duration of an epoch.
52 pub epoch_duration: Duration,
53 /// `q` in the dandelion paper, this is the probability that a node will be in the fluff state for
54 /// a certain epoch.
55 ///
56 /// The dandelion paper recommends to make this value small, but the smaller this value, the higher
57 /// the broadcast latency.
58 ///
59 /// It is recommended for this value to be <= `0.2`, this value *MUST* be in range `0.0..=1.0`.
60 pub fluff_probability: f64,
61 /// The graph type.
62 pub graph: Graph,
63}
64
65impl DandelionConfig {
66 /// Returns the number of outbound peers to use to stem transactions.
67 ///
68 /// This value depends on the [`Graph`] chosen.
69 pub const fn number_of_stems(&self) -> usize {
70 match self.graph {
71 Graph::Line => 1,
72 Graph::FourRegular => 2,
73 }
74 }
75
76 /// Returns the average embargo timeout, `Tbase` in the dandelion++ paper.
77 ///
78 /// This is the average embargo timeout _only including this node_ with `k` nodes also putting an embargo timeout
79 /// using the exponential distribution, the average until one of them fluffs is `Tbase / k`.
80 pub fn average_embargo_timeout(&self) -> Duration {
81 // we set k equal to the expected stem length with this fluff probability.
82 let k = self.expected_stem_length();
83 let time_between_hop = self.time_between_hop.as_secs_f64();
84
85 Duration::from_secs_f64(
86 // (-k*(k-1)*hop)/(2*ln(1-ep))
87 ((k.neg() * (k - 1.0) * time_between_hop)
88 / EMBARGO_FULL_TRAVEL_PROBABILITY.ln().mul(2.0))
89 .ceil(),
90 )
91 }
92
93 /// Returns the expected length of a stem.
94 pub const fn expected_stem_length(&self) -> f64 {
95 self.fluff_probability.recip()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use std::{
102 f64::consts::E,
103 ops::{Mul, Neg},
104 time::Duration,
105 };
106
107 use proptest::{prop_assert, proptest};
108
109 use super::*;
110
111 #[test]
112 fn monerod_average_embargo_timeout() {
113 let cfg = DandelionConfig {
114 time_between_hop: Duration::from_millis(175),
115 epoch_duration: Default::default(),
116 fluff_probability: 0.125,
117 graph: Default::default(),
118 };
119
120 assert_eq!(cfg.average_embargo_timeout(), Duration::from_secs(47));
121 }
122
123 proptest! {
124 #[test]
125 fn embargo_full_travel_probablity_correct(time_between_hop in 1_u64..1_000_000, fluff_probability in 0.000001..1.0) {
126 let cfg = DandelionConfig {
127 time_between_hop: Duration::from_millis(time_between_hop),
128 epoch_duration: Default::default(),
129 fluff_probability,
130 graph: Default::default(),
131 };
132
133 // assert that the `average_embargo_timeout` is high enough that the probability of `k` nodes
134 // not diffusing before expected diffusion is greater than or equal to `EMBARGO_FULL_TRAVEL_PROBABLY`
135 //
136 // using the formula from in appendix B.5
137 let k = cfg.expected_stem_length();
138 let time_between_hop = cfg.time_between_hop.as_secs_f64();
139
140 let average_embargo_timeout = cfg.average_embargo_timeout().as_secs_f64();
141
142 let probability =
143 E.powf((k.neg() * (k - 1.0) * time_between_hop) / average_embargo_timeout.mul(2.0));
144
145 prop_assert!(probability >= EMBARGO_FULL_TRAVEL_PROBABILITY, "probability = {probability}, average_embargo_timeout = {average_embargo_timeout}");
146 }
147 }
148}