1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
use super::*;

/// Quantum time used in the standard topology
pub const STANDARD_QUANTUM_TIME: Duration = Duration::from_secs(60 * 5);

/// Topology defines the structure of spacetime, in particular how space and
/// time are quantized.
///
/// Any calculation which requires converting from absolute coordinates to
/// quantized coordinates must refer to the topology. Therefore, this type is
/// ubiquitous! More functions than not take it as a parameter. This may seem
/// cumbersome, but there are a few reasons why this is helpful:
/// - We currently use a "standard" quantization for all networks, but we may
///   find it beneficial in the future to let each network specify its own
///   quantization levels, based on its own traffic and longevity needs.
/// - It is confusing to be working with three different coordinate systems in
///   this codebase, and the presence of a `&topo` param in a function is a
///   helpful reminder to be extra mindful about the unit conversions that are
///   happening
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Topology {
    /// The quantization of space
    pub space: Dimension,
    /// The quantization of time
    pub time: Dimension,
    /// The origin of time, meaning the 0th quantum contains this Timestamp.
    pub time_origin: Timestamp,
    /// Ignore any data which lies after `Timestamp::now() - time_cutoff`.
    /// This is so that historical quantized gossip does not overlap with
    /// recent gossip.
    pub time_cutoff: Duration,
}

impl Topology {
    /// Unit dimensions with the given time origin
    #[cfg(feature = "test_utils")]
    pub fn unit(time_origin: Timestamp) -> Self {
        Self {
            space: Dimension::unit(),
            time: Dimension::unit(),
            time_origin,
            time_cutoff: Duration::ZERO,
        }
    }

    /// Unit dimensions with a zero time origin
    #[cfg(feature = "test_utils")]
    pub fn unit_zero() -> Self {
        Self {
            space: Dimension::unit(),
            time: Dimension::unit(),
            time_origin: Timestamp::from_micros(0),
            time_cutoff: Duration::ZERO,
        }
    }

    /// Standard dimensions with the given time origin
    pub fn standard(time_origin: Timestamp, time_cutoff: Duration) -> Self {
        Self {
            space: Dimension::standard_space(),
            time: Dimension::standard_time(),
            time_origin,
            time_cutoff,
        }
    }

    /// Standard dimensions with the [`HOLOCHAIN_EPOCH`](Timestamp::HOLOCHAIN_EPOCH) as the time origin
    pub fn standard_epoch(time_cutoff: Duration) -> Self {
        Self::standard(Timestamp::HOLOCHAIN_EPOCH, time_cutoff)
    }

    /// Standard dimensions with the [`HOLOCHAIN_EPOCH`](Timestamp::HOLOCHAIN_EPOCH) as the time origin
    pub fn standard_epoch_full() -> Self {
        Self::standard(Timestamp::HOLOCHAIN_EPOCH, Duration::ZERO)
    }

    /// Standard dimensions with a zero time origin
    #[cfg(feature = "test_utils")]
    pub fn standard_zero() -> Self {
        Self::standard(Timestamp::ZERO, Duration::ZERO)
    }

    /// Returns the space quantum which contains this location
    pub fn space_quantum(&self, x: Loc) -> SpaceQuantum {
        (x.as_u32() / self.space.quantum).into()
    }

    /// Returns the time quantum which contains this timestamp
    pub fn time_quantum(&self, t: Timestamp) -> TimeQuantum {
        let t = (t.as_micros() - self.time_origin.as_micros()).max(0);
        ((t / self.time.quantum as i64) as u32).into()
    }

    /// Returns the time quantum which contains this timestamp
    pub fn time_quantum_duration(&self, d: std::time::Duration) -> TimeQuantum {
        ((d.as_micros() as i64 / self.time.quantum as i64) as u32).into()
    }

    /// The minimum power to use in "exponentional coordinates".
    pub fn min_space_power(&self) -> u8 {
        // If space.quantum_power is 0, then min has to be at least 1, because
        // in that case we can talk about 2^32 quanta at power 0, which would
        // overflow a `u32`.
        //
        // If space.quantum_power is greater than 0 (the standard is 12), then
        // the min power can be 0.
        1u8.saturating_sub(self.space.quantum_power)
    }

    /// The maximum power to use in "exponentional coordinates".
    /// This is 17 for standard space topology. (32 - 12 - 3)
    pub fn max_space_power(&self, strat: &ArqStrat) -> u8 {
        32 - self.space.quantum_power - strat.max_chunks_log2()
    }
}

/// Defines the quantization of a dimension of spacetime.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Dimension {
    /// The smallest possible length in this dimension.
    /// Determines the interval represented by the leaf of a tree.
    pub quantum: u32,

    /// The smallest power of 2 which is larger than the quantum.
    /// Needed for various calculations.
    pub quantum_power: u8,

    /// The log2 size of this dimension, so that 2^bit_depth is the number of
    /// possible values that can be represented.
    pub(super) bit_depth: u8,
}

impl Dimension {
    /// No quantization.
    /// Used for testing, making it easier to construct values without thinking
    /// of unit conversions.
    #[cfg(feature = "test_utils")]
    pub fn unit() -> Self {
        Dimension {
            quantum: 1,
            quantum_power: 0,
            bit_depth: 32,
        }
    }

    /// The standard space quantum size is 2^12
    pub const fn standard_space() -> Self {
        let quantum_power = 12;
        Dimension {
            // if a network has 1 million peers,
            // the average spacing between them is ~4,300
            // so at a target coverage of 100,
            // each arc will be ~430,000 in length
            // which divided by 16 (max chunks) is ~2700, which is about 2^15.
            // So, we'll go down to 2^12 just to be extra safe.
            // This means we only need 20 bits to represent any location.
            quantum: 2u32.pow(quantum_power as u32),
            quantum_power,
            bit_depth: 32 - quantum_power,
        }
    }

    /// The standard time quantum size is 5 minutes (300 million microseconds)
    pub const fn standard_time() -> Self {
        let quantum = STANDARD_QUANTUM_TIME.as_micros() as u32;
        Dimension {
            // 5 minutes in microseconds = 1mil * 60 * 5 = 300,000,000
            // log2 of this is 28.16, FYI
            quantum,
            quantum_power: 29,

            // 12 quanta = 1 hour.
            // If we set the max lifetime for a network to ~100 years, which
            // is 12 * 24 * 365 * 100 = 10,512,000 time quanta,
            // the log2 of which is 23.32,
            // then we can store any time coordinate in that range using 24 bits.
            //
            // BTW, the log2 of 100 years in microseconds is 54.81
            bit_depth: 24,
        }
    }

    /// Calculate from a quantum size
    pub fn time(quantum_dur: Duration) -> Self {
        let quantum = quantum_dur.as_micros() as u32;
        let quantum_power = ((quantum as f64).log2().ceil() as u32).try_into().unwrap();
        let quanta_per_100_years = 60 * 60 / quantum_dur.as_secs() * 24 * 365 * 100;
        let bit_depth = ((quanta_per_100_years as f64).log2().ceil() as u32)
            .try_into()
            .unwrap();
        Dimension {
            quantum,
            quantum_power,
            bit_depth,
        }
    }
}

/// Node-specific parameters for gossip.
/// While the [`Topology`] must be the same for every node in a network, each
/// node is free to choose its own GossipParams.
///
/// Choosing smaller values for these offsets can lead to less resource usage,
/// at the expense of reducing opportunities to gossip with other nodes.
/// This is also largely dependent on the characteristcs of the network,
/// since if almost all nodes are operating with the same current timestamp
/// and Arq power level, there will be very little need for reconciliation.
///
/// In networks where nodes are offline for long periods of time, or latency
/// is very high (e.g. sneakernet), it could be helpful to increase these values.
#[derive(Copy, Clone, Debug, derive_more::Constructor)]
pub struct GossipParams {
    /// What +/- coordinate offset will you accept for timestamps?
    /// e.g. if the time quantum is 5 min,
    /// a time buffer of 2 will allow +/- 10 min discrepancies with gossip partners.
    pub max_time_offset: TimeQuantum,

    /// What difference in power will you accept for other agents' Arqs?
    /// e.g. if the power I use in my arq is 14, and this offset is 2,
    /// I won't talk to anyone whose arq is expressed with a power lower
    /// than 12 or greater than 16
    pub max_space_power_offset: u8,
}

impl GossipParams {
    /// Zero-tolerance gossip params
    pub fn zero() -> Self {
        Self {
            max_time_offset: 0.into(),
            max_space_power_offset: 0,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn custom_quantum_time() {
        assert_eq!(
            Dimension::standard_time(),
            Dimension::time(STANDARD_QUANTUM_TIME)
        );
    }
}