Skip to content

Commit d9ee266

Browse files
authored
feat(kad): New provider record update strategy
In `MemoryStore`, the number of provider records per key is limited by `max_providers_per_key`. Former implementations keep provider records sorted by their distance to the key, and only keep those with the smallest distance. This strategy is vulnerable to Sybil attack, in which an attacker can flood the network with false identities in order to eclipse a key. This commit change the strategy to simply keep old providers and ignore new ones. This new strategy however, can cause load imbalance, but can be mitigated by increasing `max_providers_per_key`. In addition, old implementations failed to keep `provided` and `providers` in sync, and this commit fixes this issue. Pull-Request: #5536.
1 parent 0861e39 commit d9ee266

File tree

7 files changed

+79
-63
lines changed

7 files changed

+79
-63
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ libp2p-floodsub = { version = "0.45.0", path = "protocols/floodsub" }
8686
libp2p-gossipsub = { version = "0.47.0", path = "protocols/gossipsub" }
8787
libp2p-identify = { version = "0.45.0", path = "protocols/identify" }
8888
libp2p-identity = { version = "0.2.9" }
89-
libp2p-kad = { version = "0.46.0", path = "protocols/kad" }
89+
libp2p-kad = { version = "0.46.1", path = "protocols/kad" }
9090
libp2p-mdns = { version = "0.46.0", path = "protocols/mdns" }
9191
libp2p-memory-connection-limits = { version = "0.3.0", path = "misc/memory-connection-limits" }
9292
libp2p-metrics = { version = "0.14.2", path = "misc/metrics" }

libp2p/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.54.1
2+
3+
- Update individual crates.
4+
- Update to [`libp2p-kad` `v0.46.1`](protocols/kad/CHANGELOG.md#0461).
5+
16
## 0.54.0
27

38
- Update individual crates.

libp2p/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "libp2p"
33
edition = "2021"
44
rust-version = { workspace = true }
55
description = "Peer-to-peer networking library"
6-
version = "0.54.0"
6+
version = "0.54.1"
77
authors = ["Parity Technologies <[email protected]>"]
88
license = "MIT"
99
repository = "https://github.com/libp2p/rust-libp2p"

protocols/kad/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.46.1
2+
3+
- Use new provider record update strategy to prevent Sybil attack.
4+
See [PR 5536](https://github.com/libp2p/rust-libp2p/pull/5536).
5+
16
## 0.46.0
27

38
- Included multiaddresses of found peers alongside peer IDs in `GetClosestPeers` query results.

protocols/kad/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "libp2p-kad"
33
edition = "2021"
44
rust-version = { workspace = true }
55
description = "Kademlia protocol for libp2p"
6-
version = "0.46.0"
6+
version = "0.46.1"
77
authors = ["Parity Technologies <[email protected]>"]
88
license = "MIT"
99
repository = "https://github.com/libp2p/rust-libp2p"

protocols/kad/src/record/store/memory.rs

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -152,38 +152,31 @@ impl RecordStore for MemoryStore {
152152
}
153153
.or_insert_with(Default::default);
154154

155-
if let Some(i) = providers.iter().position(|p| p.provider == record.provider) {
156-
// In-place update of an existing provider record.
157-
providers.as_mut()[i] = record;
158-
} else {
159-
// It is a new provider record for that key.
160-
let local_key = self.local_key;
161-
let key = kbucket::Key::new(record.key.clone());
162-
let provider = kbucket::Key::from(record.provider);
163-
if let Some(i) = providers.iter().position(|p| {
164-
let pk = kbucket::Key::from(p.provider);
165-
provider.distance(&key) < pk.distance(&key)
166-
}) {
167-
// Insert the new provider.
168-
if local_key.preimage() == &record.provider {
155+
for p in providers.iter_mut() {
156+
if p.provider == record.provider {
157+
// In-place update of an existing provider record.
158+
if self.local_key.preimage() == &record.provider {
159+
self.provided.remove(p);
169160
self.provided.insert(record.clone());
170161
}
171-
providers.insert(i, record);
172-
// Remove the excess provider, if any.
173-
if providers.len() > self.config.max_providers_per_key {
174-
if let Some(p) = providers.pop() {
175-
self.provided.remove(&p);
176-
}
177-
}
178-
} else if providers.len() < self.config.max_providers_per_key {
179-
// The distance of the new provider to the key is larger than
180-
// the distance of any existing provider, but there is still room.
181-
if local_key.preimage() == &record.provider {
182-
self.provided.insert(record.clone());
183-
}
184-
providers.push(record);
162+
*p = record;
163+
return Ok(());
185164
}
186165
}
166+
167+
// If the providers list is full, we ignore the new provider.
168+
// This strategy can mitigate Sybil attacks, in which an attacker
169+
// floods the network with fake provider records.
170+
if providers.len() == self.config.max_providers_per_key {
171+
return Ok(());
172+
}
173+
174+
// Otherwise, insert the new provider record.
175+
if self.local_key.preimage() == &record.provider {
176+
self.provided.insert(record.clone());
177+
}
178+
providers.push(record);
179+
187180
Ok(())
188181
}
189182

@@ -202,7 +195,9 @@ impl RecordStore for MemoryStore {
202195
let providers = e.get_mut();
203196
if let Some(i) = providers.iter().position(|p| &p.provider == provider) {
204197
let p = providers.remove(i);
205-
self.provided.remove(&p);
198+
if &p.provider == self.local_key.preimage() {
199+
self.provided.remove(&p);
200+
}
206201
}
207202
if providers.is_empty() {
208203
e.remove();
@@ -221,11 +216,6 @@ mod tests {
221216
fn random_multihash() -> Multihash<64> {
222217
Multihash::wrap(SHA_256_MH, &rand::thread_rng().gen::<[u8; 32]>()).unwrap()
223218
}
224-
225-
fn distance(r: &ProviderRecord) -> kbucket::Distance {
226-
kbucket::Key::new(r.key.clone()).distance(&kbucket::Key::from(r.provider))
227-
}
228-
229219
#[test]
230220
fn put_get_remove_record() {
231221
fn prop(r: Record) {
@@ -250,30 +240,6 @@ mod tests {
250240
quickcheck(prop as fn(_))
251241
}
252242

253-
#[test]
254-
fn providers_ordered_by_distance_to_key() {
255-
fn prop(providers: Vec<kbucket::Key<PeerId>>) -> bool {
256-
let mut store = MemoryStore::new(PeerId::random());
257-
let key = Key::from(random_multihash());
258-
259-
let mut records = providers
260-
.into_iter()
261-
.map(|p| ProviderRecord::new(key.clone(), p.into_preimage(), Vec::new()))
262-
.collect::<Vec<_>>();
263-
264-
for r in &records {
265-
assert!(store.add_provider(r.clone()).is_ok());
266-
}
267-
268-
records.sort_by_key(distance);
269-
records.truncate(store.config.max_providers_per_key);
270-
271-
records == store.providers(&key).to_vec()
272-
}
273-
274-
quickcheck(prop as fn(_) -> _)
275-
}
276-
277243
#[test]
278244
fn provided() {
279245
let id = PeerId::random();
@@ -302,6 +268,46 @@ mod tests {
302268
assert_eq!(vec![rec.clone()], store.providers(&rec.key).to_vec());
303269
}
304270

271+
#[test]
272+
fn update_provided() {
273+
let prv = PeerId::random();
274+
let mut store = MemoryStore::new(prv);
275+
let key = random_multihash();
276+
let mut rec = ProviderRecord::new(key, prv, Vec::new());
277+
assert!(store.add_provider(rec.clone()).is_ok());
278+
assert_eq!(
279+
vec![Cow::Borrowed(&rec)],
280+
store.provided().collect::<Vec<_>>()
281+
);
282+
rec.expires = Some(Instant::now());
283+
assert!(store.add_provider(rec.clone()).is_ok());
284+
assert_eq!(
285+
vec![Cow::Borrowed(&rec)],
286+
store.provided().collect::<Vec<_>>()
287+
);
288+
}
289+
290+
#[test]
291+
fn max_providers_per_key() {
292+
let config = MemoryStoreConfig::default();
293+
let key = kbucket::Key::new(Key::from(random_multihash()));
294+
295+
let mut store = MemoryStore::with_config(PeerId::random(), config.clone());
296+
let peers = (0..config.max_providers_per_key)
297+
.map(|_| PeerId::random())
298+
.collect::<Vec<_>>();
299+
for peer in peers {
300+
let rec = ProviderRecord::new(key.preimage().clone(), peer, Vec::new());
301+
assert!(store.add_provider(rec).is_ok());
302+
}
303+
304+
// The new provider cannot be added because the key is already saturated.
305+
let peer = PeerId::random();
306+
let rec = ProviderRecord::new(key.preimage().clone(), peer, Vec::new());
307+
assert!(store.add_provider(rec.clone()).is_ok());
308+
assert!(!store.providers(&rec.key).contains(&rec));
309+
}
310+
305311
#[test]
306312
fn max_provided_keys() {
307313
let mut store = MemoryStore::new(PeerId::random());

0 commit comments

Comments
 (0)