OXYGENHUB

Coverage / crates/jet/src/swarm.rs 96.2%

279 of 290 lines run, 77 of 88 functions · run #197 on main · run missed

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 {
3161×
    pub fn new(len: usize) -> Bits {
3261×
        Bits { words: vec![0; len.div_ceil(64)], len }
3361×
    }
34
35
    /// Every bit set: a peer that holds the whole image.
365×
    pub fn full(len: usize) -> Bits {
375×
        let mut b = Bits::new(len);
38171×
        (0..len).for_each(|i| b.set(i));
395×
        b
405×
    }
41
426×
    pub fn len(&self) -> usize {
436×
        self.len
446×
    }
45
460×
    pub fn is_empty(&self) -> bool {
470×
        self.len == 0
480×
    }
49
5037374×
    pub fn get(&self, i: usize) -> bool {
5137374×
        i < self.len && self.words[i / 64] & (1 << (i % 64)) != 0
5237374×
    }
53
54784×
    pub fn set(&mut self, i: usize) {
55784×
        if i < self.len {
56784×
            self.words[i / 64] |= 1 << (i % 64);
57784×
        }
58784×
    }
59
60198×
    pub fn clear(&mut self, i: usize) {
61198×
        if i < self.len {
62198×
            self.words[i / 64] &= !(1 << (i % 64));
63198×
        }
64198×
    }
65
66940×
    pub fn count(&self) -> usize {
67942×
        self.words.iter().map(|w| w.count_ones() as usize).sum()
68940×
    }
69
70
    /// Little-endian bytes, as `GET …/have` sends them.
7116×
    pub fn to_bytes(&self) -> Vec<u8> {
7218×
        let mut out: Vec<u8> = self.words.iter().flat_map(|w| w.to_le_bytes()).collect();
7316×
        out.truncate(self.len.div_ceil(8));
7416×
        out
7516×
    }
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.
7917×
    pub fn from_bytes(len: usize, bytes: &[u8]) -> Bits {
8017×
        let mut b = Bits::new(len);
8181×
        for (i, byte) in bytes.iter().enumerate().take(len.div_ceil(8)) {
8281×
            b.words[i / 8] |= (*byte as u64) << (8 * (i % 8));
8381×
        }
8417×
        if let Some(last) = b.words.last_mut() {
8517×
            if !len.is_multiple_of(64) {
8617×
                *last &= (1u64 << (len % 64)) - 1;
8717×
            }
880×
        }
8917×
        b
9017×
    }
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.
10712×
    pub fn seal(&self, key: &KeyId, secret: &SecretKey) -> String {
10812×
        let body = serde_json::to_vec(self).expect("a ticket serialises");
10912×
        let sig = secret.sign_challenge(key, &body).expect("a ticket is longer than 16 bytes");
11012×
        format!("{}.{}", hex::encode(&body), hex::encode(Signature::encode_list(&[sig])))
11112×
    }
112
113
    /// Read and check a ticket: signed by `hub`, not expired at `now_ms`.
1149×
    pub fn open(text: &str, hub: &Identity, now_ms: u64) -> Result<Ticket, String> {
1159×
        let (body, sig) = text.trim().split_once('.').ok_or("not a ticket")?;
1168×
        let body = hex::decode(body).map_err(|_| "not a ticket")?;
1178×
        let sigs = hex::decode(sig).ok().and_then(|b| Signature::decode_list(&b).ok()).ok_or("not a ticket's signature")?;
1188×
        let sig = sigs.first().ok_or("an unsigned ticket")?;
1198×
        verify_challenge(hub, sig, &body).map_err(|_| "the ticket is not signed by this runner's hub")?;
1206×
        let t: Ticket = serde_json::from_slice(&body).map_err(|_| "not a ticket")?;
1216×
        if t.hub != hub.hash().to_hex() {
1220×
            return Err("the ticket names another hub".into());
1236×
        }
1246×
        if t.expires_ms <= now_ms {
1251×
            return Err("the ticket has expired".into());
1265×
        }
1275×
        Ok(t)
1289×
    }
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.
17413×
    pub fn new(len: usize, have: Bits, seed: u64) -> Picker {
17513×
        let mut p = Picker {
17613×
            have,
17713×
            asked: vec![0; len],
17813×
            first: vec![None; len],
17913×
            peers: HashMap::new(),
18013×
            order: Vec::new(),
18113×
            holders: vec![0; len],
18213×
            coming: vec![false; len],
18313×
            progress: Instant::now(),
18413×
            seed: seed | 1,
18513×
        };
18613×
        p.reorder();
18713×
        p
18813×
    }
189
190966×
    pub fn len(&self) -> usize {
191966×
        self.asked.len()
192966×
    }
193
1940×
    pub fn is_empty(&self) -> bool {
1950×
        self.asked.is_empty()
1960×
    }
197
198937×
    pub fn missing(&self) -> usize {
199937×
        self.len() - self.have.count()
200937×
    }
201
2020×
    pub fn have(&self) -> &Bits {
2030×
        &self.have
2040×
    }
205
206
    /// A peer's have-map, new or refreshed.
2076×
    pub fn peer(&mut self, peer: usize, bits: Bits) {
2086×
        let none = Bits::new(bits.len());
2096×
        self.peer_fetching(peer, bits, none);
2106×
    }
211
212
    /// A peer's have-map and the chunks it is fetching now.
21315×
    pub fn peer_fetching(&mut self, peer: usize, have: Bits, fetching: Bits) {
21415×
        self.peers.insert(peer, (have, fetching));
21515×
        self.reorder();
21615×
    }
217
218
    /// A peer that failed: it is asked for nothing more.
2191×
    pub fn drop_peer(&mut self, peer: usize) {
2201×
        self.peers.remove(&peer);
2211×
        self.reorder();
2221×
    }
223
224566×
    fn next_rand(&mut self) -> u64 {
225
        // xorshift64: enough to spread runners that start together.
226566×
        self.seed ^= self.seed << 13;
227566×
        self.seed ^= self.seed >> 7;
228566×
        self.seed ^= self.seed << 17;
229566×
        self.seed
230566×
    }
231
23229×
    fn reorder(&mut self) {
23329×
        let mut keyed: Vec<(u16, u64, usize)> = Vec::new();
234652×
        for i in 0..self.len() {
235652×
            self.holders[i] = self.peers.values().filter(|(b, _)| b.get(i)).count().min(u16::MAX as usize) as u16;
236652×
            self.coming[i] = self.peers.values().any(|(_, f)| f.get(i));
237652×
            if !self.have.get(i) {
238566×
                let r = self.next_rand();
239566×
                keyed.push((self.holders[i], r, i));
240566×
            }
241
        }
24229×
        keyed.sort_unstable();
24329×
        self.order = keyed.into_iter().map(|(_, _, i)| i).collect();
24429×
    }
245
246
    /// The next chunk for `src` to fetch.
247900×
    pub fn pick(&mut self, src: Source) -> Pick {
248900×
        if self.missing() == 0 {
24956×
            return Pick::Done;
250844×
        }
2519728×
        let holds = |i: usize| match src {
252829×
            Source::Hub => true,
2538899×
            Source::Peer(p) => self.peers.get(&p).is_some_and(|(b, _)| b.get(i)),
2549728×
        };
255844×
        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.
25821577×
        let fresh = self.order.iter().copied().find(|&i| {
25921577×
            !self.have.get(i)
26014619×
                && self.asked[i] == 0
2619565×
                && holds(i)
262895×
                && (src != Source::Hub || stalled || self.peers.is_empty() || (self.holders[i] == 0 && !self.coming[i]))
26321577×
        });
264844×
        if let Some(i) = fresh {
265219×
            self.asked[i] += 1;
266219×
            self.first[i] = Some(src);
267219×
            return Pick::Chunk(i);
268625×
        }
269
        // The endgame: everything missing is asked for already. The hub
270
        // joins it only once the peers stall.
271625×
        if src == Source::Hub && !stalled && !self.peers.is_empty() {
27291×
            return Pick::Wait;
273534×
        }
2745596×
        let mut left = self.order.iter().copied().filter(|&i| !self.have.get(i));
2752189×
        if left.clone().all(|i| self.asked[i] > 0) {
276265×
            if let Some(i) = left.find(|&i| self.asked[i] < ENDGAME_COPIES && self.first[i] != Some(src) && holds(i)) {
27710×
                self.asked[i] += 1;
27810×
                return Pick::Chunk(i);
27966×
            }
280458×
        }
281524×
        Pick::Wait
282900×
    }
283
284
    /// Chunk `i` arrived and was checked.
285204×
    pub fn got(&mut self, i: usize) {
286204×
        self.have.set(i);
287204×
        self.asked[i] = self.asked[i].saturating_sub(1);
288204×
        self.first[i] = None;
289204×
        self.progress = Instant::now();
290204×
    }
291
292
    /// Chunk `i` did not come: it can be asked for again.
2932×
    pub fn failed(&mut self, i: usize) {
2942×
        self.asked[i] = self.asked[i].saturating_sub(1);
2952×
        if self.asked[i] == 0 {
2961×
            self.first[i] = None;
2971×
        }
2982×
    }
299
}
300
301
#[cfg(test)]
302
mod tests {
303
    use super::*;
304
305
    #[test]
3061×
    fn bits_travel_as_bytes() {
3071×
        let mut b = Bits::new(70);
3085×
        [0, 9, 63, 64, 69].iter().for_each(|&i| b.set(i));
3091×
        let back = Bits::from_bytes(70, &b.to_bytes());
3101×
        assert_eq!(back, b);
3111×
        assert_eq!(b.to_bytes().len(), 9);
3121×
        assert_eq!(Bits::from_bytes(10, &[0xff, 0xff, 0xff]).count(), 10, "bits past the list are ignored");
3131×
        assert_eq!(Bits::from_bytes(10, &[]).count(), 0, "a short answer leaves the rest unset");
3141×
        assert_eq!(Bits::full(130).count(), 130);
3151×
    }
316
317
    #[test]
3181×
    fn a_ticket_opens_only_with_its_hub_and_in_time() {
3191×
        let sk = SecretKey::generate();
3201×
        let hub = sk.identity("hub", "hub@example", 1);
3211×
        let t = Ticket { hub: hub.hash().to_hex(), runner: 7, name: "earth-1".into(), site: "home".into(), expires_ms: 10_000 };
3221×
        let text = t.seal(&hub.hash(), &sk);
3231×
        assert_eq!(Ticket::open(&text, &hub, 9_999).unwrap(), t);
3241×
        assert!(Ticket::open(&text, &hub, 10_000).unwrap_err().contains("expired"));
3251×
        let other_sk = SecretKey::generate();
3261×
        let other = other_sk.identity("other", "o@example", 1);
3271×
        assert!(Ticket::open(&text, &other, 1).is_err(), "another hub's key");
3281×
        let mut forged = text.clone();
3291×
        forged.replace_range(2..4, "ff");
3301×
        assert!(Ticket::open(&forged, &hub, 1).is_err(), "a changed body");
3311×
        assert!(Ticket::open("nonsense", &hub, 1).is_err());
3321×
    }
333
334
    #[test]
3351×
    fn peers_are_asked_for_the_rarest_chunk_they_hold() {
3361×
        let mut p = Picker::new(4, Bits::new(4), 42);
3371×
        let mut a = Bits::new(4);
3383×
        [0, 1, 2].iter().for_each(|&i| a.set(i));
3391×
        let mut b = Bits::new(4);
3402×
        [0, 1].iter().for_each(|&i| b.set(i));
3411×
        p.peer(0, a);
3421×
        p.peer(1, b);
343
        // Chunk 2 only peer 0 holds; chunk 3 nobody does.
3441×
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(2));
3451×
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(3), "the hub seeds what no peer holds");
3461×
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "and leaves the rest to the peers");
3471×
        let first = p.pick(Source::Peer(1));
3481×
        assert!(matches!(first, Pick::Chunk(0) | Pick::Chunk(1)));
3491×
    }
350
351
    #[test]
3521×
    fn the_hub_is_not_asked_for_what_a_peer_is_fetching() {
3531×
        let mut p = Picker::new(2, Bits::new(2), 4);
3541×
        let mut fetching = Bits::new(2);
3551×
        fetching.set(0);
3561×
        p.peer_fetching(0, Bits::new(2), fetching);
3571×
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(1));
3581×
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "chunk 0 is coming from the peer");
3591×
        let mut have = Bits::new(2);
3601×
        have.set(0);
3611×
        p.peer(0, have);
3621×
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0));
3631×
    }
364
365
    #[test]
3661×
    fn without_peers_the_hub_serves_everything() {
3671×
        let mut p = Picker::new(3, Bits::new(3), 1);
3681×
        let mut seen = Vec::new();
3694×
        while let Pick::Chunk(i) = p.pick(Source::Hub) {
3703×
            seen.push(i);
3713×
            p.got(i);
3723×
        }
3731×
        seen.sort();
3741×
        assert_eq!(seen, vec![0, 1, 2]);
3751×
        assert_eq!(p.pick(Source::Hub), Pick::Done);
3761×
    }
377
378
    #[test]
3791×
    fn the_endgame_never_asks_the_same_source_twice() {
3801×
        let mut p = Picker::new(1, Bits::new(1), 9);
3811×
        assert_eq!(p.pick(Source::Hub), Pick::Chunk(0));
3821×
        assert_eq!(p.pick(Source::Hub), Pick::Wait);
3831×
        p.peer(0, Bits::full(1));
3841×
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0), "a peer may race the hub for it");
3851×
        assert_eq!(p.pick(Source::Hub), Pick::Wait, "but not the hub a peer, while the peers make progress");
3861×
    }
387
388
    #[test]
3891×
    fn the_endgame_asks_a_second_source_and_a_failure_frees_the_chunk() {
3901×
        let mut p = Picker::new(1, Bits::new(1), 3);
3911×
        p.peer(0, Bits::full(1));
3921×
        p.peer(1, Bits::full(1));
3931×
        assert_eq!(p.pick(Source::Peer(0)), Pick::Chunk(0));
3941×
        assert_eq!(p.pick(Source::Peer(1)), Pick::Chunk(0), "the endgame");
3951×
        assert_eq!(p.pick(Source::Peer(1)), Pick::Wait, "at most two copies");
3961×
        p.failed(0);
3971×
        p.failed(0);
3981×
        p.drop_peer(0);
3991×
        assert_eq!(p.pick(Source::Peer(1)), Pick::Chunk(0));
4001×
        p.got(0);
4011×
        assert_eq!(p.pick(Source::Peer(1)), Pick::Done);
4021×
    }
403
404
    #[test]
4051×
    fn chunks_already_here_are_never_asked_for() {
4061×
        let mut have = Bits::new(3);
4071×
        have.set(1);
4081×
        let mut p = Picker::new(3, have, 5);
4091×
        assert_eq!(p.missing(), 2);
4101×
        let mut seen = Vec::new();
4113×
        while let Pick::Chunk(i) = p.pick(Source::Hub) {
4122×
            seen.push(i);
4132×
            p.got(i);
4142×
        }
4151×
        assert!(!seen.contains(&1));
4161×
    }
417
418
    #[test]
4191×
    fn runners_starting_together_ask_the_hub_for_different_chunks() {
4201×
        let a: Vec<_> = {
4211×
            let mut p = Picker::new(64, Bits::new(64), 11);
4228×
            (0..8).filter_map(|_| if let Pick::Chunk(i) = p.pick(Source::Hub) { Some(i) } else { None }).collect()
423
        };
4241×
        let b: Vec<_> = {
4251×
            let mut p = Picker::new(64, Bits::new(64), 12);
4268×
            (0..8).filter_map(|_| if let Pick::Chunk(i) = p.pick(Source::Hub) { Some(i) } else { None }).collect()
427
        };
4281×
        assert_ne!(a, b);
4291×
    }
430
}