emissary_core/netdb/
bucket.rs

1// Copyright 2018-2019 Parity Technologies (UK) Ltd.
2// Copyright 2023 litep2p developers
3//
4// Permission is hereby granted, free of charge, to any person obtaining a
5// copy of this software and associated documentation files (the "Software"),
6// to deal in the Software without restriction, including without limitation
7// the rights to use, copy, modify, merge, publish, distribute, sublicense,
8// and/or sell copies of the Software, and to permit persons to whom the
9// Software is furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20// DEALINGS IN THE SOFTWARE.
21
22//! Kademlia k-bucket implementation.
23
24use crate::{
25    netdb::types::{FloodFill, Key},
26    primitives::RouterId,
27};
28
29use alloc::vec::Vec;
30
31/// Logging target for the file.
32const LOG_TARGET: &str = "emissary::netdb::k-bucket";
33
34/// Kademlia k-bucket.
35pub struct KBucket {
36    /// Floodfill routers of the bucket.
37    floodfills: Vec<FloodFill>,
38}
39
40impl KBucket {
41    /// Create new [`KBucket`].
42    pub fn new() -> Self {
43        Self {
44            floodfills: Vec::with_capacity(20),
45        }
46    }
47
48    /// Try to insert `key` into [`KBucket`].
49    ///
50    /// Returns `true` if `key` already exists or if it was successfully inserted into [`KBucket`].
51    /// If the k-bucket is full, its searched for the lowest performing floodfill and if their score
52    /// is below the insertion threshold (0), the floodfill is evicted and `key` is inserted in
53    /// their place.
54    ///
55    /// Returns `false` if `key` could not be inserted into [`KBucket`].
56    pub fn try_insert(&mut self, key: Key<RouterId>) -> bool {
57        if self.floodfills.iter().any(|floodfill| floodfill.key == key) {
58            return true;
59        }
60
61        if self.floodfills.len() < 20 {
62            self.floodfills.push(FloodFill::new(key.preimage().clone()));
63            return true;
64        }
65
66        if let Some(floodfill) = self.floodfills.iter_mut().min() {
67            if floodfill.score < 0 {
68                tracing::trace!(
69                    target: LOG_TARGET,
70                    old_floodfill = %floodfill.key.preimage(),
71                    score = %floodfill.score,
72                    new_floodfill = %key.preimage(),
73                    "evicting floodfill",
74                );
75
76                floodfill.key = key;
77                floodfill.score = 0;
78
79                return true;
80            }
81        }
82
83        false
84    }
85
86    /// Adjust score of a floodfill.
87    pub fn adjust_score(&mut self, key: Key<RouterId>, adjustment: isize) {
88        match self.floodfills.iter_mut().find(|floodfill| floodfill.key == key) {
89            Some(floodfill) => {
90                tracing::trace!(
91                    target: LOG_TARGET,
92                    score = %(floodfill.score + adjustment),
93                    "router score adjusted",
94                );
95                floodfill.score += adjustment;
96            }
97            None => tracing::debug!(
98                target: LOG_TARGET,
99                router_id = %key.preimage(),
100                "cannot adjust score, router doesn't exist in the k-bucket",
101            ),
102        }
103    }
104
105    /// Get iterator over the k-bucket, sorting the k-bucket entries in increasing order
106    /// by distance.
107    pub fn closest_iter<K: Clone>(&self, target: &Key<K>) -> impl Iterator<Item = RouterId> {
108        let mut floodfills = self.floodfills.clone();
109
110        floodfills.sort_by(|a, b| target.distance(&a.key).cmp(&target.distance(&b.key)));
111        floodfills.into_iter().map(|router| router.key.preimage().clone())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn closest_iter() {
121        let mut bucket = KBucket::new();
122
123        // add some random nodes to the bucket
124        let _ = (0..10)
125            .map(|_| {
126                let peer = RouterId::random();
127                bucket.floodfills.push(FloodFill::new(RouterId::random()));
128
129                peer
130            })
131            .collect::<Vec<_>>();
132
133        let target = Key::from(RouterId::random());
134        let mut iter = bucket.closest_iter(&target);
135        let mut prev = None;
136
137        while let Some(router_id) = iter.next() {
138            if let Some(distance) = prev {
139                assert!(distance < target.distance(&Key::from(router_id.clone())));
140            }
141
142            prev = Some(target.distance(&Key::from(router_id)));
143        }
144    }
145
146    #[test]
147    fn floodfill_with_low_score_evicted() {
148        let mut bucket = KBucket::new();
149
150        let floodfills = (0..20)
151            .map(|_| {
152                let peer = RouterId::random();
153                bucket.floodfills.push(FloodFill::new(peer.clone()));
154
155                peer
156            })
157            .collect::<Vec<_>>();
158
159        // try to add new floodfill router to k-bucket
160        let router_id = RouterId::random();
161        let key = Key::from(router_id.clone());
162        assert!(!bucket.try_insert(key.clone()));
163
164        // decrease the score of one of the floodfills
165        bucket.adjust_score(Key::from(floodfills[0].clone()), -10);
166
167        // try to insert the router again and verify it succeeds
168        assert!(bucket.try_insert(key));
169
170        // ensure the first floodfill is no longer found and that all scores are equal
171        assert!(!bucket
172            .floodfills
173            .iter()
174            .any(|floodfill| floodfill.key == Key::from(floodfills[0].clone())));
175        assert!(bucket.floodfills.iter().all(|floodfill| floodfill.score == 0));
176    }
177}