melnet2 0.3.3

JSON-RPC/nanorpc-based, low-level auto-peering RPC protocol used throughout Themelio for peer-to-peer communciations
Documentation
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
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
use std::{
    convert::Infallible,
    sync::{Arc, Weak},
    time::{Duration, Instant},
};

use anyhow::Context;
use async_trait::async_trait;
use futures_util::TryFutureExt;
use itertools::Itertools;
use nanorpc::{DynRpcTransport, OrService, RpcService, RpcTransport};
use smol::{lock::RwLock, Task};
use smol_timeout::TimeoutExt;

use crate::{
    protocol::{Address, ControlClient, ControlProtocol, ControlService},
    Backhaul,
};

use self::routedb::RouteDb;

const ROUTE_LIMIT: usize = 32;

/// Represents a node in an independent P2P swarm that implements a particular RPC protocol.
///
/// This is generic over a backhaul, so different transports can be plugged in easily, as well as a client-level handle (e.g. the `FoobarClient` that `nanorpc`'s macros generate from a `FoobarProtocol` trait). We are intentionally generic over the second one to statically prevent confusion between swarms that run incompatible RPC protocols; the combination of backhaul and client uniquely identifies a network compatible with a particular protocol.
pub struct Swarm<B: Backhaul, C> {
    /// Backhaul
    haul: Arc<B>,
    /// Route storage. Protected by an *async* RwLock
    routes: Arc<smol::lock::RwLock<RouteDb>>,
    /// Something that creates an RpcClient from a backhaul RpcTransport
    open_client: Arc<dyn Fn(DynRpcTransport) -> C + Sync + Send + 'static>,
    /// Swarm ID
    swarm_id: String,

    /// The route maintenance task
    _route_maintain_task: Arc<Task<Infallible>>,
}

impl<B: Backhaul, C> Clone for Swarm<B, C> {
    fn clone(&self) -> Self {
        Self {
            haul: self.haul.clone(),
            routes: self.routes.clone(),
            open_client: self.open_client.clone(),
            swarm_id: self.swarm_id.clone(),
            _route_maintain_task: self._route_maintain_task.clone(),
        }
    }
}

impl<B: Backhaul, C: 'static> Swarm<B, C>
where
    <B::RpcTransport as RpcTransport>::Error: std::error::Error + Send + Sync,
{
    /// Creates a new [Swarm] given a backhaul, and a function that maps a raw transport to a client-level handle.
    pub fn new(
        backhaul: B,
        client_map_fn: impl Fn(DynRpcTransport) -> C + Sync + Send + 'static,
        swarm_id: &str,
    ) -> Self {
        let haul = Arc::new(backhaul);
        let routes = Arc::new(smol::lock::RwLock::new(RouteDb::default()));
        let open_client = Arc::new(client_map_fn);
        Self {
            haul: haul.clone(),
            routes: routes.clone(),
            open_client: open_client.clone(),
            swarm_id: swarm_id.to_string(),

            _route_maintain_task: smolscale::spawn(Self::route_maintain(
                haul,
                routes,
                open_client,
                swarm_id.to_string(),
            ))
            .into(),
        }
    }

    /// Obtains a connection to a peer.
    pub async fn connect(&self, addr: Address) -> Result<C, B::ConnectError> {
        Ok((self.open_client)(DynRpcTransport::new(
            self.haul.connect(addr).await?,
        )))
    }

    /// Obtains a connection to a peer that defers connection failures to use-time automatically reconnects.
    pub async fn connect_lazy(&self, addr: Address) -> Result<C, B::ConnectError> {
        Ok((self.open_client)(DynRpcTransport::new(
            self.haul.connect_lazy(addr).await,
        )))
    }

    /// Starts a listener on the given address. If `advertise_addr` is present, then advertise this address to peers asking for routes.
    pub async fn start_listen(
        &self,
        listen_addr: Address,
        advertise_addr: Option<Address>,
        service: impl RpcService,
    ) -> Result<(), B::ListenError> {
        self.haul
            .start_listen(
                listen_addr,
                OrService::new(
                    service,
                    ControlService(ControlProtocolImpl {
                        swarm_id: self.swarm_id.clone(),
                        routes: self.routes.clone(),
                        weak_haul: Arc::downgrade(&self.haul),
                    }),
                ),
            )
            .await?;
        if let Some(advertise_addr) = advertise_addr {
            self.routes
                .write()
                .await
                .insert(advertise_addr, Duration::from_millis(0), true)
        }
        Ok(())
    }

    /// Obtains routes.
    pub async fn routes(&self) -> Vec<Address> {
        self.routes
            .read()
            .await
            .random_iter()
            .map(|s| s.addr)
            .collect_vec()
    }

    /// Adds a route, specifying whether or not it's "sticky" (will never be evicted)
    pub async fn add_route(&self, addr: Address, sticky: bool) {
        let mut v = self.routes.write().await;
        v.insert(addr, Duration::from_secs(1), sticky);
    }

    /// Background loop for route maintenance.
    #[allow(clippy::redundant_locals)]
    async fn route_maintain(
        haul: Arc<B>,
        routes: Arc<smol::lock::RwLock<RouteDb>>,
        _open_client: Arc<dyn Fn(DynRpcTransport) -> C + Sync + Send + 'static>,
        swarm_id: String,
    ) -> Infallible {
        const PULSE: Duration = Duration::from_secs(1);
        let mut timer = smol::Timer::interval(PULSE);
        let exec = smol::Executor::new();
        // we run timers on a roughly Poisson distribution in order to avoid synchronized patterns emerging
        exec.run(async {
            loop {
                if fastrand::f64() * 3.0 < 1.0 {
                    log::debug!("[{swarm_id}] push pulse");
                    if let Some(random) = routes.read().await.random_iter().next() {
                        if let Some(to_send) = routes
                            .read()
                            .await
                            .random_iter()
                            .find(|r| r.addr != random.addr)
                        {
                            let random = random.addr;
                            let to_send = to_send.addr;
                            log::debug!("[{swarm_id}] push {to_send} => {random}");
                            let random2 = random.clone();
                            exec.spawn(
                                async {
                                    let to_send = to_send;
                                    let random = random;
                                    let conn = ControlClient(
                                        haul.connect(random)
                                            .timeout(Duration::from_secs(60))
                                            .await
                                            .context("connect timeout")??,
                                    );
                                    conn.__mn_advertise_peer(to_send)
                                        .timeout(Duration::from_secs(60))
                                        .await
                                        .context("advertise timeout")??;
                                    anyhow::Ok(())
                                }
                                .unwrap_or_else(|e| {
                                    let random2 = random2;
                                    log::warn!("[{swarm_id}] push failed to {}: {e}", random2)
                                }),
                            )
                            .detach();
                        }
                    }
                }

                if fastrand::f64() * 10.0 < 1.0 {
                    let current_count = routes.read().await.count();
                    log::debug!("[{swarm_id}] pull pulse {current_count}/{ROUTE_LIMIT}");
                    if current_count < ROUTE_LIMIT {
                        // we request more routes from a random peer
                        if let Some(route) = routes.read().await.random_iter().next() {
                            log::debug!("[{swarm_id}] getting more routes from {}", route.addr);
                            let route2 = route.clone();
                            exec.spawn(
                                async {
                                    let route = route;
                                    let conn = ControlClient(
                                        haul.connect(route.addr.clone())
                                            .timeout(Duration::from_secs(60))
                                            .await
                                            .context("connect timeout")??,
                                    );
                                    for peer in conn
                                        .__mn_get_random_peers()
                                        .timeout(Duration::from_secs(60))
                                        .await
                                        .context("get peers timeout")??
                                    {
                                        log::debug!(
                                            "[{swarm_id}] got route {} from {}",
                                            route.addr,
                                            peer
                                        );
                                        let ping = test_ping(&haul, peer.clone(), &swarm_id)
                                            .await
                                            .context("ping failed")?;
                                        routes.write().await.insert(peer, ping, false)
                                    }
                                    anyhow::Ok(())
                                }
                                .unwrap_or_else(|e| {
                                    let route = route2;
                                    log::warn!(
                                        "[{swarm_id}] get more routes failed from {}: {e}",
                                        route.addr
                                    )
                                }),
                            )
                            .detach();
                        } else if current_count > ROUTE_LIMIT {
                            // delete the worst route
                            let mut routes = routes.write().await;
                            routes.remove_worst();
                        }
                    }
                }
                if fastrand::f64() * 300.0 < 1.0 {
                    log::debug!("[{swarm_id}] ping pulse...");
                    let routes_guard = routes.read().await;
                    for route in routes_guard.random_iter() {
                        exec.spawn(async {
                            let route = route;
                            if let Err(err) = test_ping(&haul, route.addr.clone(), &swarm_id).await
                            {
                                if route.sticky {
                                    log::debug!(
                                        "[{swarm_id}] keeping sticky {} despite ping-fail: {err}",
                                        route.addr
                                    );
                                } else {
                                    log::debug!(
                                        "[{swarm_id}] ping-failing non-sticky {}: {err}",
                                        route.addr
                                    );
                                    routes.write().await.remove(route.addr);
                                }
                            }
                        })
                        .detach();
                    }
                }
                (&mut timer).await;
            }
        })
        .await
    }
}

struct ControlProtocolImpl<B: Backhaul> {
    swarm_id: String,
    routes: Arc<RwLock<RouteDb>>,
    weak_haul: Weak<B>,
}

#[async_trait]
impl<B: Backhaul> ControlProtocol for ControlProtocolImpl<B>
where
    <B::RpcTransport as RpcTransport>::Error: std::error::Error + Send + Sync,
{
    async fn __mn_get_swarm_id(&self) -> String {
        self.swarm_id.clone()
    }

    async fn __mn_get_random_peers(&self) -> Vec<Address> {
        self.routes
            .read()
            .await
            .random_iter()
            .take(8)
            .map(|r| r.addr)
            .collect()
    }

    async fn __mn_advertise_peer(&self, addr: Address) -> bool {
        if self.routes.read().await.count() >= ROUTE_LIMIT {
            return false;
        }
        if let Some(haul) = self.weak_haul.upgrade() {
            if let Ok(ping) = test_ping(&haul, addr.clone(), &self.swarm_id).await {
                self.routes.write().await.insert(addr, ping, false);
            }
        }
        return true;
    }
}

async fn test_ping<B: Backhaul>(
    haul: &Arc<B>,
    addr: Address,
    swarm_id: &str,
) -> anyhow::Result<Duration>
where
    <B::RpcTransport as RpcTransport>::Error: std::error::Error + Send + Sync,
{
    let start = Instant::now();
    let client = ControlClient(
        haul.connect(addr)
            .timeout(Duration::from_secs(5))
            .await
            .context("connect timed out after 5 seconds")??,
    );
    let their_swarm_id = client
        .__mn_get_swarm_id()
        .timeout(Duration::from_secs(5))
        .await
        .context("ping timed out after 5 seconds")??;
    if their_swarm_id != swarm_id {
        anyhow::bail!(
            "their swarm ID {:?} is not our swarm ID {:?}",
            their_swarm_id,
            swarm_id
        );
    }
    Ok(start.elapsed())
}

mod routedb {
    use itertools::Itertools;

    use super::*;
    #[derive(Default)]
    pub struct RouteDb {
        /// An assoc-vector of routes. This structure is used because it enables efficient random access.
        routes: Vec<Route>,
    }

    impl RouteDb {
        pub fn get_route(&self, addr: Address) -> Option<Route> {
            self.routes.iter().find(|r| r.addr == addr).cloned()
        }

        fn get_route_mut(&mut self, addr: Address) -> Option<&mut Route> {
            self.routes.iter_mut().find(|r| r.addr == addr)
        }

        pub fn insert(&mut self, addr: Address, ping: Duration, sticky: bool) {
            if let Some(r) = self.get_route_mut(addr.clone()) {
                r.last_ping = ping;
                r.last_seen = Instant::now();
                r.sticky = sticky;
            } else {
                self.routes.push(Route {
                    addr,
                    last_ping: ping,
                    last_seen: Instant::now(),
                    sticky,
                })
            }
        }

        pub fn random_iter(&self) -> impl Iterator<Item = Route> + '_ {
            std::iter::repeat_with(|| fastrand::usize(0..self.routes.len()))
                .map(|i| self.routes[i].clone())
                .unique()
                .take(self.routes.len())
        }

        pub fn remove(&mut self, addr: Address) {
            self.routes.retain(|s| s.addr != addr);
        }

        pub fn remove_worst(&mut self) {
            self.routes.sort_unstable_by_key(|s| s.last_ping);
            let _ = self.routes.pop();
        }

        pub fn count(&self) -> usize {
            self.routes.len()
        }
    }

    #[derive(Clone, Hash, PartialEq, Eq)]
    pub struct Route {
        pub addr: Address,
        pub last_ping: Duration,
        pub last_seen: Instant,
        pub sticky: bool,
    }
}