OXYGENHUB
# 0077c7488e5d oxygenflow / crates / jet / src / swarm.rs
15.1 KiB · Rust · last changed Jetstream: runners share images peer to peer 24 days ago source blame history raw
1
//! The swarm (oxygenflow#47, docs/jetstream.md §8.1): runners fetch image
2
//! chunks from each other as well as from the hub.
3
//!
4
//! This module holds what both ends agree on and what can be tested
5
//! without a network: the have-map a peer offers ([`Bits`]), the ticket the
6
//! hub signs to admit a runner to its peers ([`Ticket`]), and the order in
7
//! which a fetcher asks its sources for chunks ([`Picker`]): rarest first,
8
//! the hub only for what no peer has, and an endgame at the close.
9
10
use oxy_core::hash::KeyId;
11
use oxy_core::identity::{verify_challenge, Identity, SecretKey, Signature};
12
use serde::{Deserialize, Serialize};
13
use std::collections::HashMap;
14
use std::time::{Duration, Instant};
15
16
/// How long a ticket admits a runner to its peers.
17
pub const TICKET_LIFE_MS: u64 = 15 * 60 * 1000;
18
/// Peers make no progress for this long and the hub is asked for anything.
19
pub const STALL: Duration = Duration::from_secs(2);
20
/// Most sources asked for the same chunk in the endgame.
21
const ENDGAME_COPIES: u8 = 2;
22
23
/// One bit per chunk of a manifest's needed list, in its order.
24
#[derive(Clone, Debug, Default, PartialEq, Eq)]
25
pub struct Bits {
26
    words: Vec<u64>,
27
    len: usize,
28
}
29
30
impl Bits {
31
    pub fn new(len: usize) -> Bits {
32
        Bits { words: vec![0; len.div_ceil(64)], len }
33
    }
34
35
    /// Every bit set: a peer that holds the whole image.
36
    pub fn full(len: usize) -> Bits {
37
        let mut b = Bits::new(len);
38
        (0..len).for_each(|i| b.set(i));
39
        b
40
    }
41
42
    pub fn len(&self) -> usize {
43
        self.len
44
    }
45
46
    pub fn is_empty(&self) -> bool {
47
        self.len == 0
48
    }
49
50
    pub fn get(&self, i: usize) -> bool {
51
        i < self.len && self.words[i / 64] & (1 << (i % 64)) != 0
52
    }
53
54
    pub fn set(&mut self, i: usize) {
55
        if i < self.len {
56
            self.words[i / 64] |= 1 << (i % 64);
57
        }
58
    }
59
60
    pub fn clear(&mut self, i: usize) {
61
        if i < self.len {
62
            self.words[i / 64] &= !(1 << (i % 64));
63
        }
64
    }
65
66
    pub fn count(&self) -> usize {
67
        self.words.iter().map(|w| w.count_ones() as usize).sum()
68
    }
69
70
    /// Little-endian bytes, as `GET …/have` sends them.
71
    pub fn to_bytes(&self) -> Vec<u8> {
72
        let mut out: Vec<u8> = self.words.iter().flat_map(|w| w.to_le_bytes()).collect();
73
        out.truncate(self.len.div_ceil(8));
74
        out
75
    }
76
77
    /// The bits of a `have` answer for a list of `len` chunks; bits past
78
    /// `len` are ignored, and a short answer means the rest are unset.
79
    pub fn from_bytes(len: usize, bytes: &[u8]) -> Bits {
80
        let mut b = Bits::new(len);
81
        for (i, byte) in bytes.iter().enumerate().take(len.div_ceil(8)) {
82
            b.words[i / 8] |= (*byte as u64) << (8 * (i % 8));
83
        }
84
        if let Some(last) = b.words.last_mut() {
85
            if !len.is_multiple_of(64) {
86
                *last &= (1u64 << (len % 64)) - 1;
87
            }
88
        }
89
        b
90
    }
91
}
92
93
/// What the hub signs to admit a runner to its peers.
94
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95
pub struct Ticket {
96
    /// The hub's key, hex: a peer accepts only its own hub's tickets.
97
    pub hub: String,
98
    pub runner: i64,
99
    pub name: String,
100
    pub site: String,
101
    pub expires_ms: u64,
102
}
103
104
impl Ticket {
105
    /// The ticket as text: its JSON and the hub's signature over it, hex,
106
    /// joined by a dot.
107
    pub fn seal(&self, key: &KeyId, secret: &SecretKey) -> String {
108
        let body = serde_json::to_vec(self).expect("a ticket serialises");
109
        let sig = secret.sign_challenge(key, &body).expect("a ticket is longer than 16 bytes");
110
        format!("{}.{}", hex::encode(&body), hex::encode(Signature::encode_list(&[sig])))
111
    }
112
113
    /// Read and check a ticket: signed by `hub`, not expired at `now_ms`.
114
    pub fn open(text: &str, hub: &Identity, now_ms: u64) -> Result<Ticket, String> {
115
        let (body, sig) = text.trim().split_once('.').ok_or("not a ticket")?;
116
        let body = hex::decode(body).map_err(|_| "not a ticket")?;
117
        let sigs = hex::decode(sig).ok().and_then(|b| Signature::decode_list(&b).ok()).ok_or("not a ticket's signature")?;
118
        let sig = sigs.first().ok_or("an unsigned ticket")?;
119
        verify_challenge(hub, sig, &body).map_err(|_| "the ticket is not signed by this runner's hub")?;
120
        let t: Ticket = serde_json::from_slice(&body).map_err(|_| "not a ticket")?;
121
        if t.hub != hub.hash().to_hex() {
122
            return Err("the ticket names another hub".into());
123
        }
124
        if t.expires_ms <= now_ms {
125
            return Err("the ticket has expired".into());
126
        }
127
        Ok(t)
128
    }
129
}
130
131
/// Where a chunk comes from: the hub, or a peer by its index.
132
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
133
pub enum Source {
134
    Hub,
135
    Peer(usize),
136
}
137
138
/// What a source should do next.
139
#[derive(Debug, PartialEq, Eq)]
140
pub enum Pick {
141
    /// Ask for this chunk (an index into the needed list).
142
    Chunk(usize),
143
    /// Nothing for this source now; ask again shortly.
144
    Wait,
145
    /// Every chunk is here.
146
    Done,
147
}
148
149
/// The order a fetcher asks its sources for chunks (docs/jetstream.md
150
/// §8.1): the rarest chunk a peer holds first; the hub for chunks no peer
151
/// holds, or for anything once the peers stall; and in the endgame, a
152
/// second source for a chunk already asked for.
153
pub struct Picker {
154
    have: Bits,
155
    asked: Vec<u8>,
156
    /// The source first asked for each chunk in flight: the endgame asks
157
    /// a different one.
158
    first: Vec<Option<Source>>,
159
    /// Each peer's map: what it has, and what it is fetching now.
160
    peers: HashMap<usize, (Bits, Bits)>,
161
    /// Missing chunks, rarest first; rebuilt when the peers' maps change.
162
    order: Vec<usize>,
163
    /// How many peers hold each chunk, as of the last rebuild.
164
    holders: Vec<u16>,
165
    /// Whether some peer is fetching each chunk now: the hub is not asked
166
    /// for it, the peer will have it shortly.
167
    coming: Vec<bool>,
168
    progress: Instant,
169
    seed: u64,
170
}
171
172
impl Picker {
173
    /// A picker for `len` chunks, of which `have` are already here.
174
    pub fn new(len: usize, have: Bits, seed: u64) -> Picker {
175
        let mut p = Picker {
176
            have,
177
            asked: vec![0; len],
178
            first: vec![None; len],
179
            peers: HashMap::new(),
180
            order: Vec::new(),
181
            holders: vec![0; len],
182
            coming: vec![false; len],
183
            progress: Instant::now(),
184
            seed: seed | 1,
185
        };
186
        p.reorder();
187
        p
188
    }
189
190
    pub fn len(&self) -> usize {
191
        self.asked.len()
192
    }
193
194
    pub fn is_empty(&self) -> bool {
195
        self.asked.is_empty()
196
    }
197
198
    pub fn missing(&self) -> usize {
199
        self.len() - self.have.count()
200
    }
201
202
    pub fn have(&self) -> &Bits {
203
        &self.have
204
    }
205
206
    /// A peer's have-map, new or refreshed.
207
    pub fn peer(&mut self, peer: usize, bits: Bits) {
208
        let none = Bits::new(bits.len());
209
        self.peer_fetching(peer, bits, none);
210
    }
211
212
    /// A peer's have-map and the chunks it is fetching now.
213
    pub fn peer_fetching(&mut self, peer: usize, have: Bits, fetching: Bits) {
214
        self.peers.insert(peer, (have, fetching));
215
        self.reorder();
216
    }
217
218
    /// A peer that failed: it is asked for nothing more.
219
    pub fn drop_peer(&mut self, peer: usize) {
220
        self.peers.remove(&peer);
221
        self.reorder();
222
    }
223
224
    fn next_rand(&mut self) -> u64 {
225
        // xorshift64: enough to spread runners that start together.
226
        self.seed ^= self.seed << 13;
227
        self.seed ^= self.seed >> 7;
228
        self.seed ^= self.seed << 17;
229
        self.seed
230
    }
231
232
    fn reorder(&mut self) {
233
        let mut keyed: Vec<(u16, u64, usize)> = Vec::new();
234
        for i in 0..self.len() {
235
            self.holders[i] = self.peers.values().filter(|(b, _)| b.get(i)).count().min(u16::MAX as usize) as u16;
236
            self.coming[i] = self.peers.values().any(|(_, f)| f.get(i));
237
            if !self.have.get(i) {
238
                let r = self.next_rand();
239
                keyed.push((self.holders[i], r, i));
240
            }
241
        }
242
        keyed.sort_unstable();
243
        self.order = keyed.into_iter().map(|(_, _, i)| i).collect();
244
    }
245
246
    /// The next chunk for `src` to fetch.
247
    pub fn pick(&mut self, src: Source) -> Pick {
248
        if self.missing() == 0 {
249
            return Pick::Done;
250
        }
251
        let holds = |i: usize| match src {
252
            Source::Hub => true,
253
            Source::Peer(p) => self.peers.get(&p).is_some_and(|(b, _)| b.get(i)),
254
        };
255
        let stalled = self.progress.elapsed() >= STALL;
256
        // Not yet asked for: a peer takes the rarest it holds; the hub
257
        // takes what no peer holds, or anything once the peers stall.
258
        let fresh = self.order.iter().copied().find(|&i| {
259
            !self.have.get(i)
260
                && self.asked[i] == 0
261
                && holds(i)
262
                && (src != Source::Hub || stalled || self.peers.is_empty() || (self.holders[i] == 0 && !self.coming[i]))
263
        });
264
        if let Some(i) = fresh {
265
            self.asked[i] += 1;
266
            self.first[i] = Some(src);
267
            return Pick::Chunk(i);
268
        }
269
        // The endgame: everything missing is asked for already. The hub
270
        // joins it only once the peers stall.
271
        if src == Source::Hub && !stalled && !self.peers.is_empty() {
272
            return Pick::Wait;
273
        }
274
        let mut left = self.order.iter().copied().filter(|&i| !self.have.get(i));
275
        if left.clone().all(|i| self.asked[i] > 0) {
276
            if let Some(i) = left.find(|&i| self.asked[i] < ENDGAME_COPIES && self.first[i] != Some(src) && holds(i)) {
277
                self.asked[i] += 1;
278
                return Pick::Chunk(i);
279
            }
280
        }
281
        Pick::Wait
282
    }
283
284
    /// Chunk `i` arrived and was checked.
285
    pub fn got(&mut self, i: usize) {
286
        self.have.set(i);
287
        self.asked[i] = self.asked[i].saturating_sub(1);
288
        self.first[i] = None;
289
        self.progress = Instant::now();
290
    }
291
292
    /// Chunk `i` did not come: it can be asked for again.
293
    pub fn failed(&mut self, i: usize) {
294
        self.asked[i] = self.asked[i].saturating_sub(1);
295
        if self.asked[i] == 0 {
296
            self.first[i] = None;
297
        }
298
    }
299
}
300
301
#[cfg(test)]
302
mod tests {
303
    use super::*;
304
305
    #[test]
306
    fn bits_travel_as_bytes() {
307
        let mut b = Bits::new(70);
308
        [0, 9, 63, 64, 69].iter().for_each(|&i| b.set(i));
309
        let back = Bits::from_bytes(70, &b.to_bytes());
310
        assert_eq!(back, b);
311
        assert_eq!(b.to_bytes().len(), 9);
312
        assert_eq!(Bits::from_bytes(10, &[0xff, 0xff, 0xff]).count(), 10, "bits past the list are ignored");
313
        assert_eq!(Bits::from_bytes(10, &[]).count(), 0, "a short answer leaves the rest unset");
314
        assert_eq!(Bits::full(130).count(), 130);
315
    }
316
317
    #[test]
318
    fn a_ticket_opens_only_with_its_hub_and_in_time() {
319
        let sk = SecretKey::generate();
320
        let hub = sk.identity("hub", "hub@example", 1);
321
        let t = Ticket { hub: hub.hash().to_hex(), runner: 7, name: "earth-1".into(), site: "home".into(), expires_ms: 10_000 };
322
        let text = t.seal(&hub.hash(), &sk);
323
        assert_eq!(Ticket::open(&text, &hub, 9_999).unwrap(), t);
324
        assert!(Ticket::open(&text, &hub, 10_000).unwrap_err().contains("expired"));
325
        let other_sk = SecretKey::generate();
326
        let other = other_sk.identity("other", "o@example", 1);
327
        assert!(Ticket::open(&text, &other, 1).is_err(), "another hub's key");
328
        let mut forged = text.clone();
329
        forged.replace_range(2..4, "ff");
330
        assert!(Ticket::open(&forged, &hub, 1).is_err(), "a changed body");
331
        assert!(Ticket::open("nonsense", &hub, 1).is_err());
332
    }
333
334
    #[test]
335
    fn peers_are_asked_for_the_rarest_chunk_they_hold() {
336
        let mut p = Picker::new(4, Bits::new(4), 42);
337
        let mut a = Bits::new(4);
338
        [0, 1, 2].iter().for_each(|&i| a.set(i));
339
        let mut b = Bits::new(4);
340
        [0, 1].iter().for_each(|&i| b.set(i));
341
        p.peer(0, a);
342
        p.peer(1, b);
343
        // Chunk 2 only peer 0 holds; chunk 3 nobody does.
344
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(2));
345
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(3), "the hub seeds what no peer holds");
346
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "and leaves the rest to the peers");
347
        let first = p.pick(Source::Peer(1));
348
        assert!(matches!(first, Pick::Chunk(0) | Pick::Chunk(1)));
349
    }
350
351
    #[test]
352
    fn the_hub_is_not_asked_for_what_a_peer_is_fetching() {
353
        let mut p = Picker::new(2, Bits::new(2), 4);
354
        let mut fetching = Bits::new(2);
355
        fetching.set(0);
356
        p.peer_fetching(0, Bits::new(2), fetching);
357
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(1));
358
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "chunk 0 is coming from the peer");
359
        let mut have = Bits::new(2);
360
        have.set(0);
361
        p.peer(0, have);
362
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0));
363
    }
364
365
    #[test]
366
    fn without_peers_the_hub_serves_everything() {
367
        let mut p = Picker::new(3, Bits::new(3), 1);
368
        let mut seen = Vec::new();
369
        while let Pick::Chunk(i) = p.pick(Source::Hub) {
370
            seen.push(i);
371
            p.got(i);
372
        }
373
        seen.sort();
374
        assert_eq!(seen, vec![0, 1, 2]);
375
        assert_eq!(p.pick(Source::Hub), Pick::Done);
376
    }
377
378
    #[test]
379
    fn the_endgame_never_asks_the_same_source_twice() {
380
        let mut p = Picker::new(1, Bits::new(1), 9);
381
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(0));
382
        assert_eq!(p.pick(Source::Hub), Pick::Wait);
383
        p.peer(0, Bits::full(1));
384
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0), "a peer may race the hub for it");
385
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "but not the hub a peer, while the peers make progress");
386
    }
387
388
    #[test]
389
    fn the_endgame_asks_a_second_source_and_a_failure_frees_the_chunk() {
390
        let mut p = Picker::new(1, Bits::new(1), 3);
391
        p.peer(0, Bits::full(1));
392
        p.peer(1, Bits::full(1));
393
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0));
394
        assert_eq!(p.pick(Source::Peer(1)), Pick::Chunk(0), "the endgame");
395
        assert_eq!(p.pick(Source::Peer(1)), Pick::Wait, "at most two copies");
396
        p.failed(0);
397
        p.failed(0);
398
        p.drop_peer(0);
399
        assert_eq!(p.pick(Source::Peer(1)), Pick::Chunk(0));
400
        p.got(0);
401
        assert_eq!(p.pick(Source::Peer(1)), Pick::Done);
402
    }
403
404
    #[test]
405
    fn chunks_already_here_are_never_asked_for() {
406
        let mut have = Bits::new(3);
407
        have.set(1);
408
        let mut p = Picker::new(3, have, 5);
409
        assert_eq!(p.missing(), 2);
410
        let mut seen = Vec::new();
411
        while let Pick::Chunk(i) = p.pick(Source::Hub) {
412
            seen.push(i);
413
            p.got(i);
414
        }
415
        assert!(!seen.contains(&1));
416
    }
417
418
    #[test]
419
    fn runners_starting_together_ask_the_hub_for_different_chunks() {
420
        let a: Vec<_> = {
421
            let mut p = Picker::new(64, Bits::new(64), 11);
422
            (0..8).filter_map(|_| if let Pick::Chunk(i) = p.pick(Source::Hub) { Some(i) } else { None }).collect()
423
        };
424
        let b: Vec<_> = {
425
            let mut p = Picker::new(64, Bits::new(64), 12);
426
            (0..8).filter_map(|_| if let Pick::Chunk(i) = p.pick(Source::Hub) { Some(i) } else { None }).collect()
427
        };
428
        assert_ne!(a, b);
429
    }
430
}