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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
//! Adaptive TTL management for large-scale mesh networks.
//!
//! This module implements adaptive TTL calculation that scales with network size
//! to ensure messages can reach distant nodes while preventing excessive flooding.
use crate::constants::*;
/// Configuration for adaptive TTL behavior.
#[derive(Debug, Clone)]
pub struct AdaptiveTtlConfig {
/// Base TTL for small networks.
pub base_ttl: u8,
/// Additional TTL per 100 devices in the network.
pub ttl_per_100_devices: u8,
/// Maximum TTL to prevent infinite propagation.
pub max_ttl: u8,
/// Minimum TTL to ensure basic propagation.
pub min_ttl: u8,
/// TTL boost for congested/queued messages.
pub congestion_boost: u8,
/// Network size threshold below which base TTL is used.
pub small_network_threshold: usize,
/// Enable adaptive TTL (if false, always use base_ttl).
pub enabled: bool,
}
impl Default for AdaptiveTtlConfig {
fn default() -> Self {
Self {
base_ttl: ADAPTIVE_TTL_BASE,
ttl_per_100_devices: ADAPTIVE_TTL_PER_100_DEVICES,
max_ttl: ADAPTIVE_TTL_MAX,
min_ttl: ADAPTIVE_TTL_MIN,
congestion_boost: ADAPTIVE_TTL_CONGESTION_BOOST,
small_network_threshold: ADAPTIVE_TTL_SMALL_NETWORK_THRESHOLD,
enabled: true,
}
}
}
/// Network size estimate for TTL calculation.
#[derive(Debug, Clone, Default)]
pub struct NetworkSizeEstimate {
/// Number of directly connected peers.
pub connected_peers: usize,
/// Number of peers seen in advertisements (1-hop neighborhood).
pub visible_peers: usize,
/// Estimated total network size (may be derived from gossip or heuristics).
pub estimated_total: Option<usize>,
/// Current congestion level (0.0-1.0).
pub congestion_level: f32,
/// Whether we've been experiencing delivery failures.
pub has_delivery_failures: bool,
}
impl NetworkSizeEstimate {
/// Creates a new network size estimate.
pub fn new(connected_peers: usize, visible_peers: usize) -> Self {
Self {
connected_peers,
visible_peers,
estimated_total: None,
congestion_level: 0.0,
has_delivery_failures: false,
}
}
/// Sets the estimated total network size.
pub fn with_estimated_total(mut self, total: usize) -> Self {
self.estimated_total = Some(total);
self
}
/// Sets the congestion level.
pub fn with_congestion(mut self, level: f32) -> Self {
self.congestion_level = level.clamp(0.0, 1.0);
self
}
/// Marks that delivery failures have occurred.
pub fn with_delivery_failures(mut self) -> Self {
self.has_delivery_failures = true;
self
}
/// Estimates network size using heuristics if not explicitly set.
///
/// Uses a simple model: if each peer connects to ~4 others on average,
/// the total network size is approximately visible_peers * 4 / degree.
/// This is a rough estimate and should be replaced with actual gossip-based
/// network size estimation in production.
fn estimated_size(&self) -> usize {
if let Some(total) = self.estimated_total {
return total;
}
// Heuristic: assume each node has ~4 connections on average
// So visible peers represent roughly 1/4 of the network
// But be conservative and assume we see at least 1/2
let min_estimate = self.visible_peers.max(self.connected_peers);
let max_estimate = self.visible_peers.saturating_mul(4);
// Use geometric mean as a reasonable middle ground
((min_estimate as f64 * max_estimate as f64).sqrt() as usize).max(min_estimate)
}
}
/// Adaptive TTL calculator.
pub struct AdaptiveTtlCalculator {
config: AdaptiveTtlConfig,
}
impl AdaptiveTtlCalculator {
/// Creates a new calculator with default configuration.
pub fn new() -> Self {
Self::with_config(AdaptiveTtlConfig::default())
}
/// Creates a new calculator with custom configuration.
pub fn with_config(config: AdaptiveTtlConfig) -> Self {
Self { config }
}
/// Computes the optimal TTL for a message based on network conditions.
///
/// # Arguments
///
/// * `network_estimate` - Current network size estimate
/// * `is_priority_message` - Whether this is a high-priority message
/// * `is_queued` - Whether this message has been queued (retry scenario)
///
/// # Returns
///
/// The recommended TTL value for the message.
pub fn compute_ttl(
&self,
network_estimate: &NetworkSizeEstimate,
is_priority_message: bool,
is_queued: bool,
) -> u8 {
if !self.config.enabled {
return self.config.base_ttl;
}
let estimated_size = network_estimate.estimated_size();
// Start with base TTL
let mut ttl = self.config.base_ttl;
// Add TTL based on network size. `extra_hundreds` is clamped before
// the narrowing cast: at ~25,650 devices it reaches 256, and an
// unclamped `as u8` would wrap to 0, silently dropping the entire
// size-based boost.
if estimated_size > self.config.small_network_threshold {
let extra_hundreds = (estimated_size - self.config.small_network_threshold) / 100;
let additional_ttl = u8::try_from(extra_hundreds)
.unwrap_or(u8::MAX)
.saturating_mul(self.config.ttl_per_100_devices);
ttl = ttl.saturating_add(additional_ttl);
}
// Boost TTL for queued messages (they've already lost time)
if is_queued {
ttl = ttl.saturating_add(self.config.congestion_boost);
}
// Boost TTL if we're experiencing delivery failures
if network_estimate.has_delivery_failures {
ttl = ttl.saturating_add(1);
}
// Boost TTL for priority messages
if is_priority_message {
ttl = ttl.saturating_add(2);
}
// Reduce TTL slightly in high congestion to prevent amplifying the problem
if network_estimate.congestion_level > 0.7 && !is_priority_message {
ttl = ttl.saturating_sub(1);
}
// Clamp to valid range
ttl.clamp(self.config.min_ttl, self.config.max_ttl)
}
/// Computes TTL for a reply message (e.g., ACK).
///
/// Reply TTL should match the hop count of the incoming message
/// to ensure the reply can reach the original sender.
pub fn compute_reply_ttl(
&self,
incoming_hop_count: u8,
network_estimate: &NetworkSizeEstimate,
) -> u8 {
// Reply needs at least as many hops as it took to get here
let min_reply_ttl = incoming_hop_count.saturating_add(2); // +2 for safety margin
// Compute normal TTL
let normal_ttl = self.compute_ttl(network_estimate, false, false);
// Use the larger of the two
min_reply_ttl.max(normal_ttl).min(self.config.max_ttl)
}
/// Checks if a message's TTL is sufficient for the current network.
///
/// Returns the recommended boost if TTL seems too low, or 0 if sufficient.
pub fn recommend_ttl_boost(
&self,
current_ttl: u8,
network_estimate: &NetworkSizeEstimate,
) -> u8 {
let optimal_ttl = self.compute_ttl(network_estimate, false, false);
if current_ttl >= optimal_ttl {
0
} else {
optimal_ttl.saturating_sub(current_ttl)
}
}
/// Gets the current configuration.
pub fn config(&self) -> &AdaptiveTtlConfig {
&self.config
}
}
impl Default for AdaptiveTtlCalculator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_ttl_for_small_network() {
let calc = AdaptiveTtlCalculator::new();
let estimate = NetworkSizeEstimate::new(4, 10);
let ttl = calc.compute_ttl(&estimate, false, false);
assert_eq!(ttl, 8); // Base TTL for small network
}
#[test]
fn test_increased_ttl_for_large_network() {
let calc = AdaptiveTtlCalculator::new();
let estimate = NetworkSizeEstimate::new(4, 100).with_estimated_total(500);
let ttl = calc.compute_ttl(&estimate, false, false);
// 500 devices - 50 threshold = 450 extra, 450/100 = 4 extra TTL units
// Base 8 + 4*2 = 16
assert!(ttl > 8);
assert!(ttl <= 24);
}
#[test]
fn test_ttl_boost_for_queued_message() {
let calc = AdaptiveTtlCalculator::new();
let estimate = NetworkSizeEstimate::new(4, 20);
let normal_ttl = calc.compute_ttl(&estimate, false, false);
let queued_ttl = calc.compute_ttl(&estimate, false, true);
assert!(queued_ttl > normal_ttl);
}
#[test]
fn test_ttl_boost_for_priority_message() {
let calc = AdaptiveTtlCalculator::new();
let estimate = NetworkSizeEstimate::new(4, 20);
let normal_ttl = calc.compute_ttl(&estimate, false, false);
let priority_ttl = calc.compute_ttl(&estimate, true, false);
assert!(priority_ttl > normal_ttl);
}
#[test]
fn test_ttl_reduction_in_congestion() {
let calc = AdaptiveTtlCalculator::new();
let normal_estimate = NetworkSizeEstimate::new(4, 20);
let congested_estimate = NetworkSizeEstimate::new(4, 20).with_congestion(0.8);
let normal_ttl = calc.compute_ttl(&normal_estimate, false, false);
let congested_ttl = calc.compute_ttl(&congested_estimate, false, false);
assert!(congested_ttl <= normal_ttl);
}
#[test]
fn test_ttl_clamped_to_max() {
let calc = AdaptiveTtlCalculator::new();
let huge_estimate = NetworkSizeEstimate::new(4, 1000).with_estimated_total(10000);
let ttl = calc.compute_ttl(&huge_estimate, true, true);
assert!(ttl <= 24); // Max TTL
}
#[test]
fn test_ttl_no_wrap_at_u8_boundary() {
let calc = AdaptiveTtlCalculator::new();
// 25,650 devices: (25650 - 50) / 100 = 256, which wraps to 0 under a
// bare `as u8` cast — the size boost would vanish and TTL would fall
// back to base instead of max.
let boundary = NetworkSizeEstimate::new(4, 100).with_estimated_total(25_650);
assert_eq!(calc.compute_ttl(&boundary, false, false), 24);
// Far past the boundary the boost must stay saturated, not oscillate.
let huge = NetworkSizeEstimate::new(4, 100).with_estimated_total(10_000_000);
assert_eq!(calc.compute_ttl(&huge, false, false), 24);
}
#[test]
fn test_ttl_clamped_to_min() {
let config = AdaptiveTtlConfig {
base_ttl: 2, // Very low base
..Default::default()
};
let calc = AdaptiveTtlCalculator::with_config(config);
let estimate = NetworkSizeEstimate::new(2, 5).with_congestion(0.9);
let ttl = calc.compute_ttl(&estimate, false, false);
assert!(ttl >= 4); // Min TTL
}
#[test]
fn test_reply_ttl_matches_hop_count() {
let calc = AdaptiveTtlCalculator::new();
let estimate = NetworkSizeEstimate::new(4, 20);
// If message took 6 hops, reply needs at least 8 (6 + 2 safety margin)
let reply_ttl = calc.compute_reply_ttl(6, &estimate);
assert!(reply_ttl >= 8);
}
#[test]
fn test_disabled_adaptive_ttl() {
let config = AdaptiveTtlConfig {
enabled: false,
base_ttl: 10,
..Default::default()
};
let calc = AdaptiveTtlCalculator::with_config(config);
let estimate = NetworkSizeEstimate::new(4, 1000).with_estimated_total(5000);
let ttl = calc.compute_ttl(&estimate, true, true);
assert_eq!(ttl, 10); // Should use base TTL regardless
}
#[test]
fn test_network_size_estimation_heuristic() {
let estimate = NetworkSizeEstimate::new(4, 50);
let size = estimate.estimated_size();
// Should estimate between visible_peers and visible_peers * 4
assert!(size >= 50);
assert!(size <= 200);
}
}