Kademlia: Address some TODOs - Refactoring - API updates. (#1174)

* Address some TODOs, refactor queries and public API.

The following left-over issues are addressed:

  * The key for FIND_NODE requests is generalised to any Multihash,
    instead of just peer IDs.
  * All queries get a (configurable) timeout.
  * Finishing queries as soon as enough results have been received is simplified
    to avoid code duplication.
  * No more panics in provider-API-related code paths. The provider API is
    however still untested and (I think) still incomplete (e.g. expiration
    of provider records).
  * Numerous smaller TODOs encountered in the code.

The following public API changes / additions are made:

  * Introduce a `KademliaConfig` with new configuration options for
    the replication factor and query timeouts.
  * Rename `find_node` to `get_closest_peers`.
  * Rename `get_value` to `get_record` and `put_value` to `put_record`,
    introducing a `Quorum` parameter for both functions, replacing the
    existing `num_results` parameter with clearer semantics.
  * Rename `add_providing` to `start_providing` and `remove_providing`
    to `stop_providing`.
  * Add a `bootstrap` function that implements a (almost) standard
    Kademlia bootstrapping procedure.
  * Rename `KademliaOut` to `KademliaEvent` with an updated list of
    constructors (some renaming). All events that report query results
    now report a `Result` to uniformly permit reporting of errors.

The following refactorings are made:

  * Introduce some constants.
  * Consolidate `query.rs` and `write.rs` behind a common query interface
    to reduce duplication and facilitate better code reuse, introducing
    the notion of a query peer iterator. `query/peers/closest.rs`
    contains the code that was formerly in `query.rs`. `query/peers/fixed.rs` contains
    a modified variant of `write.rs` (which is removed). The new `query.rs`
    provides an interface for working with a collection of queries, taking
    over some code from `behaviour.rs`.
  * Reduce code duplication in tests and use the current_thread runtime for
    polling swarms to avoid spurious errors in the test output due to aborted
    connections when a test finishes prematurely (e.g. because a quorum of
    results has been collected).
  * Some additions / improvements to the existing tests.

* Fix test.

* Fix rebase.

* Tweak kad-ipfs example.

* Incorporate some feedback.

* Provide easy access and conversion to keys in error results.
This commit is contained in:
Roman Borschel
2019-07-03 16:16:25 +02:00
committed by GitHub
parent 8af4a28152
commit ef9cb056b2
18 changed files with 2451 additions and 1662 deletions

View File

@ -20,16 +20,11 @@
#![cfg(test)]
use crate::{
GetValueResult,
Kademlia,
KademliaOut,
kbucket::{self, Distance},
record::{Record, RecordStore},
};
use futures::{future, prelude::*};
use super::*;
use crate::kbucket::Distance;
use futures::future;
use libp2p_core::{
PeerId,
Swarm,
Transport,
identity,
@ -41,10 +36,10 @@ use libp2p_core::{
};
use libp2p_secio::SecioConfig;
use libp2p_yamux as yamux;
use rand::random;
use rand::{Rng, random, thread_rng};
use std::{collections::HashSet, iter::FromIterator, io, num::NonZeroU8, u64};
use tokio::runtime::Runtime;
use multihash::Hash;
use tokio::runtime::current_thread;
use multihash::Hash::SHA2256;
type TestSwarm = Swarm<
Boxed<(PeerId, StreamMuxerBox), io::Error>,
@ -72,7 +67,8 @@ fn build_nodes(num: usize) -> (u64, Vec<TestSwarm>) {
.map_err(|e| panic!("Failed to create transport: {:?}", e))
.boxed();
let kad = Kademlia::new(local_public_key.clone().into_peer_id());
let cfg = KademliaConfig::new(local_public_key.clone().into_peer_id());
let kad = Kademlia::new(cfg);
result.push(Swarm::new(transport, kad, local_public_key.into_peer_id()));
}
@ -85,54 +81,48 @@ fn build_nodes(num: usize) -> (u64, Vec<TestSwarm>) {
(port_base, result)
}
#[test]
fn query_iter() {
fn distances(key: &kbucket::Key<PeerId>, peers: Vec<PeerId>) -> Vec<Distance> {
peers.into_iter()
.map(kbucket::Key::from)
.map(|k| k.distance(key))
.collect()
fn build_connected_nodes(total: usize, step: usize) -> (Vec<PeerId>, Vec<TestSwarm>) {
let (port_base, mut swarms) = build_nodes(total);
let swarm_ids: Vec<_> = swarms.iter().map(Swarm::local_peer_id).cloned().collect();
let mut i = 0;
for (j, peer) in swarm_ids.iter().enumerate().skip(1) {
if i < swarm_ids.len() {
swarms[i].add_address(&peer, Protocol::Memory(port_base + j as u64).into());
}
if j % step == 0 {
i += step;
}
}
fn run(n: usize) {
// Build `n` nodes. Node `n` knows about node `n-1`, node `n-1` knows about node `n-2`, etc.
// Node `n` is queried for a random peer and should return nodes `1..n-1` sorted by
// their distances to that peer.
(swarm_ids, swarms)
}
let (port_base, mut swarms) = build_nodes(n);
let swarm_ids: Vec<_> = swarms.iter().map(Swarm::local_peer_id).cloned().collect();
#[test]
fn bootstrap() {
fn run<G: rand::Rng>(rng: &mut G) {
let num_total = rng.gen_range(2, 20);
let num_group = rng.gen_range(1, num_total);
let (swarm_ids, mut swarms) = build_connected_nodes(num_total, num_group);
// Connect each swarm in the list to its predecessor in the list.
for (i, (swarm, peer)) in &mut swarms.iter_mut().skip(1).zip(swarm_ids.clone()).enumerate() {
swarm.add_address(&peer, Protocol::Memory(port_base + i as u64).into())
}
swarms[0].bootstrap();
// Ask the last peer in the list to search a random peer. The search should
// propagate backwards through the list of peers.
let search_target = PeerId::random();
let search_target_key = kbucket::Key::from(search_target.clone());
swarms.last_mut().unwrap().find_node(search_target.clone());
// Set up expectations.
let expected_swarm_id = swarm_ids.last().unwrap().clone();
let expected_peer_ids: Vec<_> = swarm_ids.iter().cloned().take(n - 1).collect();
let mut expected_distances = distances(&search_target_key, expected_peer_ids.clone());
expected_distances.sort();
// Expected known peers
let expected_known = swarm_ids.iter().skip(1).cloned().collect::<HashSet<_>>();
// Run test
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
for (i, swarm) in swarms.iter_mut().enumerate() {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::FindNodeResult {
key, closer_peers
})) => {
assert_eq!(key, search_target);
assert_eq!(swarm_ids[i], expected_swarm_id);
assert!(expected_peer_ids.iter().all(|p| closer_peers.contains(p)));
let key = kbucket::Key::from(key);
assert_eq!(expected_distances, distances(&key, closer_peers));
Async::Ready(Some(KademliaEvent::BootstrapResult(Ok(ok)))) => {
assert_eq!(i, 0);
assert_eq!(ok.peer, swarm_ids[0]);
let known = swarm.kbuckets.iter()
.map(|e| e.node.key.preimage().clone())
.collect::<HashSet<_>>();
assert_eq!(expected_known, known);
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
@ -142,10 +132,66 @@ fn query_iter() {
}
Ok(Async::NotReady)
}))
.unwrap()
}
for n in 2..=8 { run(n) }
let mut rng = thread_rng();
for _ in 0 .. 10 {
run(&mut rng)
}
}
#[test]
fn query_iter() {
fn distances<K>(key: &kbucket::Key<K>, peers: Vec<PeerId>) -> Vec<Distance> {
peers.into_iter()
.map(kbucket::Key::from)
.map(|k| k.distance(key))
.collect()
}
fn run<G: Rng>(rng: &mut G) {
let num_total = rng.gen_range(2, 20);
let (swarm_ids, mut swarms) = build_connected_nodes(num_total, 1);
// Ask the first peer in the list to search a random peer. The search should
// propagate forwards through the list of peers.
let search_target = PeerId::random();
let search_target_key = kbucket::Key::from(search_target.clone());
swarms[0].get_closest_peers(search_target.clone());
// Set up expectations.
let expected_swarm_id = swarm_ids[0].clone();
let expected_peer_ids: Vec<_> = swarm_ids.iter().skip(1).cloned().collect();
let mut expected_distances = distances(&search_target_key, expected_peer_ids.clone());
expected_distances.sort();
// Run test
current_thread::run(
future::poll_fn(move || {
for (i, swarm) in swarms.iter_mut().enumerate() {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaEvent::GetClosestPeersResult(Ok(ok)))) => {
assert_eq!(ok.key, search_target);
assert_eq!(swarm_ids[i], expected_swarm_id);
assert!(expected_peer_ids.iter().all(|p| ok.peers.contains(p)));
let key = kbucket::Key::new(ok.key);
assert_eq!(expected_distances, distances(&key, ok.peers));
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
Async::NotReady => break,
}
}
}
Ok(Async::NotReady)
}))
}
let mut rng = thread_rng();
for _ in 0 .. 10 {
run(&mut rng)
}
}
#[test]
@ -162,16 +208,16 @@ fn unresponsive_not_returned_direct() {
// Ask first to search a random value.
let search_target = PeerId::random();
swarms[0].find_node(search_target.clone());
swarms[0].get_closest_peers(search_target.clone());
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::FindNodeResult { key, closer_peers })) => {
assert_eq!(key, search_target);
assert_eq!(closer_peers.len(), 0);
Async::Ready(Some(KademliaEvent::GetClosestPeersResult(Ok(ok)))) => {
assert_eq!(ok.key, search_target);
assert_eq!(ok.peers.len(), 0);
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
@ -182,7 +228,6 @@ fn unresponsive_not_returned_direct() {
Ok(Async::NotReady)
}))
.unwrap();
}
#[test]
@ -207,17 +252,17 @@ fn unresponsive_not_returned_indirect() {
// Ask second to search a random value.
let search_target = PeerId::random();
swarms[1].find_node(search_target.clone());
swarms[1].get_closest_peers(search_target.clone());
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::FindNodeResult { key, closer_peers })) => {
assert_eq!(key, search_target);
assert_eq!(closer_peers.len(), 1);
assert_eq!(closer_peers[0], first_peer_id);
Async::Ready(Some(KademliaEvent::GetClosestPeersResult(Ok(ok)))) => {
assert_eq!(ok.key, search_target);
assert_eq!(ok.peers.len(), 1);
assert_eq!(ok.peers[0], first_peer_id);
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
@ -228,38 +273,34 @@ fn unresponsive_not_returned_indirect() {
Ok(Async::NotReady)
}))
.unwrap();
}
#[test]
fn get_value_not_found() {
fn get_record_not_found() {
let (port_base, mut swarms) = build_nodes(3);
let swarm_ids: Vec<_> = swarms.iter()
.map(|swarm| Swarm::local_peer_id(&swarm).clone()).collect();
let swarm_ids: Vec<_> = swarms.iter().map(Swarm::local_peer_id).cloned().collect();
swarms[0].add_address(&swarm_ids[1], Protocol::Memory(port_base + 1).into());
swarms[1].add_address(&swarm_ids[2], Protocol::Memory(port_base + 2).into());
let target_key = multihash::encode(Hash::SHA2256, &vec![1,2,3]).unwrap();
let num_results = NonZeroU8::new(1).unwrap();
swarms[0].get_value(&target_key, num_results);
let target_key = multihash::encode(SHA2256, &vec![1,2,3]).unwrap();
swarms[0].get_record(&target_key, Quorum::One);
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::GetValueResult(result))) => {
if let GetValueResult::NotFound { key, closest_peers } = result {
Async::Ready(Some(KademliaEvent::GetRecordResult(Err(e)))) => {
if let GetRecordError::NotFound { key, closest_peers, } = e {
assert_eq!(key, target_key);
assert_eq!(closest_peers.len(), 2);
assert!(closest_peers.contains(&swarm_ids[1]));
assert!(closest_peers.contains(&swarm_ids[2]));
return Ok(Async::Ready(()));
} else {
panic!("Expected GetValueResult::NotFound event");
panic!("Unexpected error result: {:?}", e);
}
}
Async::Ready(_) => (),
@ -270,62 +311,37 @@ fn get_value_not_found() {
Ok(Async::NotReady)
}))
.unwrap()
}
#[test]
fn put_value() {
fn run() {
// Build a test that checks if PUT_VALUE gets correctly propagated in
// a nontrivial topology:
// [31]
// / \
// [29] [30]
// /|\ /|\
// [0]..[14] [15]..[28]
//
// Nodes [29] and [30] have less than kbuckets::MAX_NODES_PER_BUCKET
// peers to avoid the situation when the bucket may be overflowed and
// some of the connections are dropped from the routing table
let (port_base, mut swarms) = build_nodes(32);
fn run<G: rand::Rng>(rng: &mut G) {
let num_total = rng.gen_range(21, 40);
let num_group = rng.gen_range(1, usize::min(num_total, kbucket::K_VALUE));
let (swarm_ids, mut swarms) = build_connected_nodes(num_total, num_group);
let swarm_ids: Vec<_> = swarms.iter()
.map(|swarm| Swarm::local_peer_id(&swarm).clone()).collect();
// Connect swarm[30] to each swarm in swarms[..15]
for (i, peer) in swarm_ids.iter().take(15).enumerate() {
swarms[30].add_address(&peer, Protocol::Memory(port_base + i as u64).into());
}
// Connect swarm[29] to each swarm in swarms[15..29]
for (i, peer) in swarm_ids.iter().skip(15).take(14).enumerate() {
swarms[29].add_address(&peer, Protocol::Memory(port_base + (i + 15) as u64).into());
}
// Connect swarms[31] to swarms[29, 30]
swarms[31].add_address(&swarm_ids[30], Protocol::Memory(port_base + 30 as u64).into());
swarms[31].add_address(&swarm_ids[29], Protocol::Memory(port_base + 29 as u64).into());
let target_key = multihash::encode(Hash::SHA2256, &vec![1,2,3]).unwrap();
let key = multihash::encode(SHA2256, &vec![1,2,3]).unwrap();
let bucket_key = kbucket::Key::from(key.clone());
let mut sorted_peer_ids: Vec<_> = swarm_ids
.iter()
.map(|id| (id.clone(), kbucket::Key::from(id.clone()).distance(&kbucket::Key::from(target_key.clone()))))
.map(|id| (id.clone(), kbucket::Key::from(id.clone()).distance(&bucket_key)))
.collect();
sorted_peer_ids.sort_by(|(_, d1), (_, d2)| d1.cmp(d2));
let closest: HashSet<PeerId> = HashSet::from_iter(sorted_peer_ids.into_iter().map(|(id, _)| id));
let closest = HashSet::from_iter(sorted_peer_ids.into_iter().map(|(id, _)| id));
swarms[31].put_value(target_key.clone(), vec![4,5,6]);
let record = Record { key: key.clone(), value: vec![4,5,6] };
swarms[0].put_record(record, Quorum::All);
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
let mut check_results = false;
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::PutValueResult{ .. })) => {
Async::Ready(Some(KademliaEvent::PutRecordResult(Ok(_)))) => {
check_results = true;
}
Async::Ready(_) => (),
@ -337,25 +353,27 @@ fn put_value() {
if check_results {
let mut have: HashSet<_> = Default::default();
for (i, swarm) in swarms.iter().take(31).enumerate() {
if swarm.records.get(&target_key).is_some() {
for (i, swarm) in swarms.iter().skip(1).enumerate() {
if swarm.records.get(&key).is_some() {
have.insert(swarm_ids[i].clone());
}
}
let intersection: HashSet<_> = have.intersection(&closest).collect();
assert_eq!(have.len(), kbucket::MAX_NODES_PER_BUCKET);
assert_eq!(intersection.len(), kbucket::MAX_NODES_PER_BUCKET);
assert_eq!(have.len(), kbucket::K_VALUE);
assert_eq!(intersection.len(), kbucket::K_VALUE);
return Ok(Async::Ready(()));
}
Ok(Async::NotReady)
}))
.unwrap()
}
let mut rng = thread_rng();
for _ in 0 .. 10 {
run();
run(&mut rng);
}
}
@ -363,36 +381,28 @@ fn put_value() {
fn get_value() {
let (port_base, mut swarms) = build_nodes(3);
let swarm_ids: Vec<_> = swarms.iter()
.map(|swarm| Swarm::local_peer_id(&swarm).clone()).collect();
let swarm_ids: Vec<_> = swarms.iter().map(Swarm::local_peer_id).cloned().collect();
swarms[0].add_address(&swarm_ids[1], Protocol::Memory(port_base + 1).into());
swarms[1].add_address(&swarm_ids[2], Protocol::Memory(port_base + 2).into());
let target_key = multihash::encode(Hash::SHA2256, &vec![1,2,3]).unwrap();
let target_value = vec![4,5,6];
let num_results = NonZeroU8::new(1).unwrap();
swarms[1].records.put(Record {
key: target_key.clone(),
value: target_value.clone()
}).unwrap();
swarms[0].get_value(&target_key, num_results);
let record = Record {
key: multihash::encode(SHA2256, &vec![1,2,3]).unwrap(),
value: vec![4,5,6]
};
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
swarms[1].records.put(record.clone()).unwrap();
swarms[0].get_record(&record.key, Quorum::One);
current_thread::run(
future::poll_fn(move || {
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::GetValueResult(result))) => {
if let GetValueResult::Found { results } = result {
assert_eq!(results.len(), 1);
let record = results.first().unwrap();
assert_eq!(record.key, target_key);
assert_eq!(record.value, target_value);
return Ok(Async::Ready(()));
} else {
panic!("Expected GetValueResult::Found event");
}
Async::Ready(Some(KademliaEvent::GetRecordResult(Ok(ok)))) => {
assert_eq!(ok.records.len(), 1);
assert_eq!(ok.records.first(), Some(&record));
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
Async::NotReady => break,
@ -402,56 +412,43 @@ fn get_value() {
Ok(Async::NotReady)
}))
.unwrap()
}
#[test]
fn get_value_multiple() {
// Check that if we have responses from multiple peers, a correct number of
// results is returned.
let num_results = NonZeroU8::new(10).unwrap();
let (port_base, mut swarms) = build_nodes(2 + num_results.get() as usize);
let num_nodes = 12;
let (_swarm_ids, mut swarms) = build_connected_nodes(num_nodes, num_nodes);
let num_results = 10;
let swarm_ids: Vec<_> = swarms.iter()
.map(|swarm| Swarm::local_peer_id(&swarm).clone()).collect();
let record = Record {
key: multihash::encode(SHA2256, &vec![1,2,3]).unwrap(),
value: vec![4,5,6],
};
let target_key = multihash::encode(Hash::SHA2256, &vec![1,2,3]).unwrap();
let target_value = vec![4,5,6];
for (i, swarm_id) in swarm_ids.iter().skip(1).enumerate() {
swarms[i + 1].records.put(Record {
key: target_key.clone(),
value: target_value.clone()
}).unwrap();
swarms[0].add_address(&swarm_id, Protocol::Memory(port_base + (i + 1) as u64).into());
for i in 0 .. num_nodes {
swarms[i].records.put(record.clone()).unwrap();
}
swarms[0].records.put(Record { key: target_key.clone(), value: target_value.clone() }).unwrap();
swarms[0].get_value(&target_key, num_results);
let quorum = Quorum::N(NonZeroU8::new(num_results as u8).unwrap());
swarms[0].get_record(&record.key, quorum);
Runtime::new().unwrap().block_on(
future::poll_fn(move || -> Result<_, io::Error> {
current_thread::run(
future::poll_fn(move || {
for swarm in &mut swarms {
loop {
match swarm.poll().unwrap() {
Async::Ready(Some(KademliaOut::GetValueResult(result))) => {
if let GetValueResult::Found { results } = result {
assert_eq!(results.len(), num_results.get() as usize);
let record = results.first().unwrap();
assert_eq!(record.key, target_key);
assert_eq!(record.value, target_value);
return Ok(Async::Ready(()));
} else {
panic!("Expected GetValueResult::Found event");
}
Async::Ready(Some(KademliaEvent::GetRecordResult(Ok(ok)))) => {
assert_eq!(ok.records.len(), num_results);
assert_eq!(ok.records.first(), Some(&record));
return Ok(Async::Ready(()));
}
Async::Ready(_) => (),
Async::NotReady => break,
}
}
}
Ok(Async::NotReady)
}))
.unwrap()
}