Skip to main content

karyon_p2p/discovery/kademlia/
bloom.rs

1use std::{hash::Hasher, sync::Arc};
2
3use bincode::{Decode, Encode};
4use parking_lot::Mutex;
5use siphasher::sip::SipHasher13;
6
7use crate::protocol::ProtocolFlags;
8
9/// 128-bit bloom filter (k=2 hashes) of items a peer supports.
10///
11/// Content-agnostic: items can be protocol ids, swarm keys, or any
12/// other identifier hashable as bytes. Discovery-layer hint, not an
13/// authoritative list.
14#[derive(Encode, Decode, Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct Bloom(u128);
16
17impl Bloom {
18    /// Insert an item.
19    pub fn add<I: AsRef<[u8]>>(&mut self, item: I) {
20        self.0 |= item_mask(item.as_ref());
21    }
22
23    /// True if this filter might contain `item`.
24    pub fn may_contain<I: AsRef<[u8]>>(&self, item: I) -> bool {
25        let mask = item_mask(item.as_ref());
26        (self.0 & mask) == mask
27    }
28
29    /// True if every bit set in `other` is also set in `self`.
30    /// An empty `other` is always covered.
31    pub fn covers(&self, other: &Self) -> bool {
32        (self.0 & other.0) == other.0
33    }
34
35    /// True if `self` and `other` share at least one bit.
36    pub fn intersects(&self, other: &Self) -> bool {
37        (self.0 & other.0) != 0
38    }
39
40    /// True if no bit is set.
41    pub fn is_empty(&self) -> bool {
42        self.0 == 0
43    }
44}
45
46/// What the local node advertises and how it filters peers.
47///
48/// `advertised` is sent on the wire. `required` and `preferred` never
49/// leave the node; they drive routing-table filtering: a peer must
50/// cover `required` and, when `preferred` is non-empty, intersect it.
51#[derive(Clone, Copy, Debug, Default)]
52pub struct LocalBloom {
53    pub advertised: Bloom,
54    pub required: Bloom,
55    pub preferred: Bloom,
56}
57
58impl LocalBloom {
59    /// True if `peer`'s advertised bloom is acceptable to this node.
60    pub fn matches(&self, peer: &Bloom) -> bool {
61        if !peer.covers(&self.required) {
62            return false;
63        }
64        if self.preferred.is_empty() {
65            return true;
66        }
67        peer.intersects(&self.preferred)
68    }
69}
70
71/// Shared handle to the local bloom state. Writers add items in
72/// place, readers take a `snapshot`. Cheap to clone (Arc).
73#[derive(Clone, Debug, Default)]
74pub struct BloomRef {
75    inner: Arc<Mutex<LocalBloom>>,
76}
77
78impl BloomRef {
79    /// Empty shared bloom.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Add an item. Any non-empty flags advertise it; `REQUIRED` and
85    /// `PREFERRED` also mark it for local filtering. Other bits only
86    /// advertise.
87    pub fn add<I: AsRef<[u8]>>(&self, item: I, flags: ProtocolFlags) {
88        if flags == ProtocolFlags::empty() {
89            return;
90        }
91        let item = item.as_ref();
92        let mut local = self.inner.lock();
93        local.advertised.add(item);
94        if flags.contains(ProtocolFlags::REQUIRED) {
95            local.required.add(item);
96        }
97        if flags.contains(ProtocolFlags::PREFERRED) {
98            local.preferred.add(item);
99        }
100    }
101
102    /// Copy of the current local bloom state.
103    pub fn snapshot(&self) -> LocalBloom {
104        *self.inner.lock()
105    }
106}
107
108/// Bit mask for `item`: two bit positions in 0..128 from
109/// siphash-1-3(item). The two halves of the 64-bit output give the
110/// two bloom hashes. Fixed zero keys: the bloom is a public,
111/// deterministic identifier every peer must compute the same way.
112fn item_mask(bytes: &[u8]) -> u128 {
113    let mut hasher = SipHasher13::new_with_keys(0, 0);
114    hasher.write(bytes);
115    let h = hasher.finish();
116    let a = (h as u32) % 128;
117    let b = ((h >> 32) as u32) % 128;
118    (1u128 << a) | (1u128 << b)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn empty_filter_contains_nothing() {
127        let b = Bloom::default();
128        assert!(b.is_empty());
129        assert!(!b.may_contain("X"));
130    }
131
132    #[test]
133    fn add_then_contains() {
134        let mut b = Bloom::default();
135        b.add("ChatProto");
136        assert!(b.may_contain("ChatProto"));
137    }
138
139    #[test]
140    fn covers_subset() {
141        let mut peer = Bloom::default();
142        peer.add("X");
143        peer.add("Y");
144
145        let mut mine = Bloom::default();
146        mine.add("X");
147        assert!(peer.covers(&mine));
148
149        mine.add("Z");
150        assert!(!peer.covers(&mine));
151    }
152
153    #[test]
154    fn empty_is_always_covered() {
155        assert!(Bloom::default().covers(&Bloom::default()));
156    }
157
158    #[test]
159    fn intersects_when_overlap() {
160        let mut peer = Bloom::default();
161        peer.add("Y");
162
163        let mut mine = Bloom::default();
164        mine.add("Y");
165        assert!(peer.intersects(&mine));
166
167        let mut other = Bloom::default();
168        other.add("Q");
169        assert!(!peer.intersects(&other));
170    }
171
172    #[test]
173    fn deterministic_across_instances() {
174        let mut a = Bloom::default();
175        a.add("SomeProto");
176        let mut b = Bloom::default();
177        b.add("SomeProto");
178        assert_eq!(a, b);
179    }
180
181    #[test]
182    fn local_matches_required_and_preferred() {
183        let bloom = BloomRef::new();
184        bloom.add("Ping", ProtocolFlags::REQUIRED);
185        bloom.add("Chat", ProtocolFlags::PREFERRED);
186        let local = bloom.snapshot();
187
188        let mut peer = Bloom::default();
189        assert!(!local.matches(&peer));
190        peer.add("Ping");
191        assert!(!local.matches(&peer));
192        peer.add("Chat");
193        assert!(local.matches(&peer));
194    }
195
196    #[test]
197    fn empty_local_matches_everything() {
198        let local = LocalBloom::default();
199        assert!(local.matches(&Bloom::default()));
200    }
201
202    #[test]
203    fn custom_flags_only_advertise() {
204        let bloom = BloomRef::new();
205        bloom.add("Room", ProtocolFlags::USER);
206        let local = bloom.snapshot();
207        assert!(local.advertised.may_contain("Room"));
208        assert!(local.required.is_empty());
209        assert!(local.preferred.is_empty());
210    }
211
212    #[test]
213    fn empty_flags_do_nothing() {
214        let bloom = BloomRef::new();
215        bloom.add("Hidden", ProtocolFlags::empty());
216        assert!(bloom.snapshot().advertised.is_empty());
217    }
218}