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}