Skip to content

Commit 56b6c62

Browse files
maqimergify[bot]
andauthored
feat(kad): expose a kad query facility allowing dynamic num_results (#5555)
## Description This PR is to expose a kad query facility that allowing specify num_results dynamically. It is related to the [Sybil Defence issue](#4769), that during the attempt of implementation on higher level code, it is find will be useful if libp2p-kad can expose such facility. The PR try not to cause any interference to the existing work flow, only introduce an `extra exposal`. ## Change checklist <!-- Please add a Changelog entry in the appropriate crates and bump the crate versions if needed. See <https://github.com/libp2p/rust-libp2p/blob/master/docs/release.md#development-between-releases>--> - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [ ] I have added tests that prove my fix is effective or that my feature works - [x] A changelog entry has been made in the appropriate crates --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent aa9317f commit 56b6c62

File tree

7 files changed

+110
-7
lines changed

7 files changed

+110
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
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.2", path = "protocols/kad" }
89+
libp2p-kad = { version = "0.47.0", 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.15.0", path = "misc/metrics" }

protocols/kad/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.47.0
2+
3+
- Expose a kad query facility allowing specify num_results dynamicly.
4+
See [PR 5555](https://github.com/libp2p/rust-libp2p/pull/5555).
5+
16
## 0.46.2
27

38
- Emit `ToSwarm::NewExternalAddrOfPeer`.

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.2"
6+
version = "0.47.0"
77
authors = ["Parity Technologies <[email protected]>"]
88
license = "MIT"
99
repository = "https://github.com/libp2p/rust-libp2p"

protocols/kad/src/behaviour.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,31 @@ where
732732
/// The result of the query is delivered in a
733733
/// [`Event::OutboundQueryProgressed{QueryResult::GetClosestPeers}`].
734734
pub fn get_closest_peers<K>(&mut self, key: K) -> QueryId
735+
where
736+
K: Into<kbucket::Key<K>> + Into<Vec<u8>> + Clone,
737+
{
738+
self.get_closest_peers_inner(key, None)
739+
}
740+
741+
/// Initiates an iterative query for the closest peers to the given key.
742+
/// The expected responding peers is specified by `num_results`
743+
/// Note that the result is capped after exceeds K_VALUE
744+
///
745+
/// The result of the query is delivered in a
746+
/// [`Event::OutboundQueryProgressed{QueryResult::GetClosestPeers}`].
747+
pub fn get_n_closest_peers<K>(&mut self, key: K, num_results: NonZeroUsize) -> QueryId
748+
where
749+
K: Into<kbucket::Key<K>> + Into<Vec<u8>> + Clone,
750+
{
751+
// The inner code never expect higher than K_VALUE results to be returned.
752+
// And removing such cap will be tricky,
753+
// since it would involve forging a new key and additional requests.
754+
// Hence bound to K_VALUE here to set clear expectation and prevent unexpected behaviour.
755+
let capped_num_results = std::cmp::min(num_results, K_VALUE);
756+
self.get_closest_peers_inner(key, Some(capped_num_results))
757+
}
758+
759+
fn get_closest_peers_inner<K>(&mut self, key: K, num_results: Option<NonZeroUsize>) -> QueryId
735760
where
736761
K: Into<kbucket::Key<K>> + Into<Vec<u8>> + Clone,
737762
{
@@ -740,6 +765,7 @@ where
740765
let info = QueryInfo::GetClosestPeers {
741766
key,
742767
step: ProgressStep::first(),
768+
num_results,
743769
};
744770
let peer_keys: Vec<kbucket::Key<PeerId>> = self.kbuckets.closest_keys(&target).collect();
745771
self.queries.add_iter_closest(target, peer_keys, info)
@@ -1485,7 +1511,7 @@ where
14851511
})
14861512
}
14871513

1488-
QueryInfo::GetClosestPeers { key, mut step } => {
1514+
QueryInfo::GetClosestPeers { key, mut step, .. } => {
14891515
step.last = true;
14901516

14911517
Some(Event::OutboundQueryProgressed {
@@ -1702,7 +1728,7 @@ where
17021728
},
17031729
}),
17041730

1705-
QueryInfo::GetClosestPeers { key, mut step } => {
1731+
QueryInfo::GetClosestPeers { key, mut step, .. } => {
17061732
step.last = true;
17071733
Some(Event::OutboundQueryProgressed {
17081734
id: query_id,
@@ -3181,6 +3207,8 @@ pub enum QueryInfo {
31813207
key: Vec<u8>,
31823208
/// Current index of events.
31833209
step: ProgressStep,
3210+
/// If required, `num_results` specifies expected responding peers
3211+
num_results: Option<NonZeroUsize>,
31843212
},
31853213

31863214
/// A (repeated) query initiated by [`Behaviour::get_providers`].

protocols/kad/src/behaviour/test.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ fn query_iter() {
263263

264264
match swarms[0].behaviour_mut().query(&qid) {
265265
Some(q) => match q.info() {
266-
QueryInfo::GetClosestPeers { key, step } => {
266+
QueryInfo::GetClosestPeers { key, step, .. } => {
267267
assert_eq!(&key[..], search_target.to_bytes().as_slice());
268268
assert_eq!(usize::from(step.count), 1);
269269
}
@@ -425,6 +425,68 @@ fn unresponsive_not_returned_indirect() {
425425
}))
426426
}
427427

428+
// Test the result of get_closest_peers with different num_results
429+
// Note that the result is capped after exceeds K_VALUE
430+
#[test]
431+
fn get_closest_with_different_num_results() {
432+
let k_value = K_VALUE.get();
433+
for replication_factor in [5, k_value / 2, k_value] {
434+
for num_results in k_value / 2..k_value * 2 {
435+
get_closest_with_different_num_results_inner(num_results, replication_factor)
436+
}
437+
}
438+
}
439+
440+
fn get_closest_with_different_num_results_inner(num_results: usize, replication_factor: usize) {
441+
let k_value = K_VALUE.get();
442+
let num_of_nodes = 3 * k_value;
443+
let mut cfg = Config::new(PROTOCOL_NAME);
444+
cfg.set_replication_factor(NonZeroUsize::new(replication_factor).unwrap());
445+
let swarms = build_connected_nodes_with_config(num_of_nodes, replication_factor - 1, cfg);
446+
447+
let mut swarms = swarms
448+
.into_iter()
449+
.map(|(_addr, swarm)| swarm)
450+
.collect::<Vec<_>>();
451+
452+
// Ask first to search a random value.
453+
let search_target = PeerId::random();
454+
let Some(num_results_nonzero) = std::num::NonZeroUsize::new(num_results) else {
455+
panic!("Unexpected NonZeroUsize val of {num_results}");
456+
};
457+
swarms[0]
458+
.behaviour_mut()
459+
.get_n_closest_peers(search_target, num_results_nonzero);
460+
461+
block_on(poll_fn(move |ctx| {
462+
for swarm in &mut swarms {
463+
loop {
464+
match swarm.poll_next_unpin(ctx) {
465+
Poll::Ready(Some(SwarmEvent::Behaviour(Event::OutboundQueryProgressed {
466+
result: QueryResult::GetClosestPeers(Ok(ok)),
467+
..
468+
}))) => {
469+
assert_eq!(&ok.key[..], search_target.to_bytes().as_slice());
470+
if num_results > k_value {
471+
assert_eq!(ok.peers.len(), k_value, "Failed with replication_factor: {replication_factor}, num_results: {num_results}");
472+
} else {
473+
assert_eq!(ok.peers.len(), num_results, "Failed with replication_factor: {replication_factor}, num_results: {num_results}");
474+
}
475+
476+
return Poll::Ready(());
477+
}
478+
// Ignore any other event.
479+
Poll::Ready(Some(_)) => (),
480+
e @ Poll::Ready(_) => panic!("Unexpected return value: {e:?}"),
481+
Poll::Pending => break,
482+
}
483+
}
484+
}
485+
486+
Poll::Pending
487+
}))
488+
}
489+
428490
#[test]
429491
fn get_record_not_found() {
430492
let mut swarms = build_nodes(3);

protocols/kad/src/query.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,16 @@ impl QueryPool {
138138
T: Into<KeyBytes> + Clone,
139139
I: IntoIterator<Item = Key<PeerId>>,
140140
{
141+
let num_results = match info {
142+
QueryInfo::GetClosestPeers {
143+
num_results: Some(val),
144+
..
145+
} => val,
146+
_ => self.config.replication_factor,
147+
};
148+
141149
let cfg = ClosestPeersIterConfig {
142-
num_results: self.config.replication_factor,
150+
num_results,
143151
parallelism: self.config.parallelism,
144152
..ClosestPeersIterConfig::default()
145153
};

0 commit comments

Comments
 (0)