summaryrefslogtreecommitdiffstats
path: root/src/swarm/pair.rs
blob: c1a196b60d03dc92ac3e9ab0853da8517fe75b10 (plain)
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
//! Which side of a pair of nodes leads, for duties only one side needs to do (sending
//! keepalive PINGs).
//!
//! Both sides decide on their own, from the two keys alone, and agree. The leader is
//! chosen per pair by one bit of a hash of both keys, not by comparing the keys: a
//! global order would make the node with the smallest key lead every one of its pairs
//! and the one with the largest lead none, so every such duty would pile up on the same
//! few nodes. With the hash, each node leads about half of its pairs.

use sha2::{Digest, Sha256};

const LABEL: &[u8] = b"continuum/pair-leader/1";

/// Whether `local` leads its pair with `peer`. Exactly one of `leads(a, b)` and
/// `leads(b, a)` is true for any two different keys.
pub(crate) fn leads(local: &str, peer: &str) -> bool {
    let (low, high) = if local <= peer {
        (local, peer)
    } else {
        (peer, local)
    };
    let mut hash = Sha256::new();
    // Length-prefixed fields, so no two different pairs hash the same input.
    for field in [LABEL, low.as_bytes(), high.as_bytes()] {
        hash.update((field.len() as u32).to_be_bytes());
        hash.update(field);
    }
    let leader = if hash.finalize()[0] & 1 == 0 {
        low
    } else {
        high
    };
    leader == local
}

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

    fn key(n: u32) -> String {
        format!("{:064x}", u64::from(n).wrapping_mul(0x9e37_79b9_7f4a_7c15))
    }

    #[test]
    fn exactly_one_side_leads() {
        for a in 0..50 {
            for b in 0..50 {
                if a != b {
                    assert_ne!(leads(&key(a), &key(b)), leads(&key(b), &key(a)));
                }
            }
        }
    }

    #[test]
    fn leading_is_spread_across_nodes() {
        let nodes: Vec<String> = (0..64).map(key).collect();
        let mut smallest_key_leads = 0;
        for (i, node) in nodes.iter().enumerate() {
            let led = nodes
                .iter()
                .filter(|peer| *peer != node && leads(node, peer))
                .count();
            // 63 pairs per node: about half each; far from all or none.
            assert!((10..=53).contains(&led), "node {i} leads {led} of 63 pairs");
            for peer in &nodes {
                if node < peer && leads(node, peer) {
                    smallest_key_leads += 1;
                }
            }
        }
        // Not simply the smaller key of each pair (64 * 63 / 2 = 2016 pairs).
        assert!(
            (800..=1216).contains(&smallest_key_leads),
            "{smallest_key_leads}"
        );
    }
}