Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/src/peer_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use std::{convert::TryFrom, fmt, str::FromStr};

/// Public keys with byte-lengths smaller than `MAX_INLINE_KEY_LENGTH` will be
/// automatically used as the peer id using an identity multihash.
//
// Note: see `from_public_key` for how this value will be used in the future.
const MAX_INLINE_KEY_LENGTH: usize = 42;

/// Identifier of a peer of the network.
Expand Down Expand Up @@ -98,7 +100,7 @@ impl PeerId {
/// returns back the data as an error.
#[inline]
pub fn from_multihash(data: multihash::Multihash) -> Result<PeerId, multihash::Multihash> {
if data.algorithm() == multihash::Hash::SHA2256 {
if data.algorithm() == multihash::Hash::SHA2256 || data.algorithm() == multihash::Hash::Identity {
Ok(PeerId { multihash: data })
} else {
Err(data)
Expand Down
69 changes: 57 additions & 12 deletions misc/mdns/src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ use libp2p_core::{Multiaddr, PeerId};
use rand;
use std::{borrow::Cow, cmp, error, fmt, str, time::Duration};

/// Maximum size of a DNS label as per RFC1035
const MAX_LABEL_LENGTH: usize = 63;

/// Decodes a `<character-string>` (as defined by RFC1035) into a `Vec` of ASCII characters.
// TODO: better error type?
pub fn decode_character_string(mut from: &[u8]) -> Result<Cow<'_, [u8]>, ()> {
Expand Down Expand Up @@ -117,23 +120,15 @@ pub fn build_query_response(
// TTL for the answer
append_u32(&mut out, ttl);

let peer_id_base58 = peer_id.to_base58();

// Peer Id.
let peer_name = format!(
"{}.{}",
data_encoding::BASE32_DNSCURVE.encode(&peer_id.into_bytes()),
str::from_utf8(SERVICE_NAME).expect("SERVICE_NAME is always ASCII")
);
let mut peer_id_bytes = Vec::with_capacity(64);
append_qname(&mut peer_id_bytes, peer_name.as_bytes());
let peer_id_bytes = encode_peer_id(&peer_id);
debug_assert!(peer_id_bytes.len() <= 0xffff);
append_u16(&mut out, peer_id_bytes.len() as u16);
out.extend_from_slice(&peer_id_bytes);

// The TXT records for answers.
for addr in addresses {
let txt_to_send = format!("dnsaddr={}/p2p/{}", addr.to_string(), peer_id_base58);
let txt_to_send = format!("dnsaddr={}/p2p/{}", addr.to_string(), peer_id.to_base58());
let mut txt_to_send_bytes = Vec::with_capacity(txt_to_send.len());
append_character_string(&mut txt_to_send_bytes, txt_to_send.as_bytes())?;
append_txt_record(&mut out, &peer_id_bytes, ttl, Some(&txt_to_send_bytes[..]))?;
Expand Down Expand Up @@ -177,7 +172,7 @@ pub fn build_service_discovery_response(id: u16, ttl: Duration) -> Vec<u8> {

// Service name.
{
let mut name = Vec::new();
let mut name = Vec::with_capacity(SERVICE_NAME.len() + 2);
append_qname(&mut name, SERVICE_NAME);
append_u16(&mut out, name.len() as u16);
out.extend_from_slice(&name);
Expand Down Expand Up @@ -211,6 +206,40 @@ fn append_u16(out: &mut Vec<u8>, value: u16) {
out.push((value & 0xff) as u8);
}

/// If a peer ID is longer than 63 characters, split it into segments to
/// be compatible with RFC 1035.
fn segment_peer_id(peer_id: String) -> String {
// Guard for the most common case
if peer_id.len() <= MAX_LABEL_LENGTH { return peer_id }

// This will only perform one allocation except in extreme circumstances.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can replace the function's body with something like this:

Suggested change
// This will only perform one allocation except in extreme circumstances.
String::from_utf8(peer_id.as_bytes().chunks(63).join(&b'.')).expect("we copy from a UTF8 string; qed")

@peat peat Oct 28, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that .chunks can't .join (in 1.38.0) ... I went through several iterations to see if I could make that work elegantly, and ended up with the for ... in loop for clarity and minimal number of allocations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked further into this as I was curious, I guess not because it really matters in this case. I am sorry if this is just useless noise to the discussion.

Under the assumption that the string only contains ASCII characters, why not do:

    let b = peer_id
        .as_bytes()
        .chunks(MAX_LABEL_LENGTH)
        .collect::<Vec<_>>()
        .join(&b'.');
    String::from_utf8(b).expect("we copy from a UTF8 string; qed")

Using a for-loop and manually preallocating the right amount of memory seems like an early optimization to me. I have benchmarked this here. Using iterators with join allows one to not copy the characters one by one as in the for loop, but instead in chunks (See https://doc.rust-lang.org/src/alloc/slice.rs.html#664).

Running the above linked benchmarks in criterion shows that the above would be twice as fast (on my machine) as the for loop. In addition I find it easier to comprehend.

@peat @tomaka what do you think?

let mut out = String::with_capacity(peer_id.len() + 8);

for (idx, chr) in peer_id.chars().enumerate() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line bothers me a bit. We know the string only contains ASCII characters, so I guess it's fine. I would have preferred using bytes.

if idx > 0 && idx % MAX_LABEL_LENGTH == 0 {
out.push('.');
}
out.push(chr);
}
out
}

/// Combines and encodes a `PeerId` and service name for a DNS query.
fn encode_peer_id(peer_id: &PeerId) -> Vec<u8> {
// DNS-safe encoding for the Peer ID
let raw_peer_id = data_encoding::BASE32_DNSCURVE.encode(&peer_id.as_bytes());
// ensure we don't have any labels over 63 bytes long
let encoded_peer_id = segment_peer_id(raw_peer_id);
let service_name = str::from_utf8(SERVICE_NAME).expect("SERVICE_NAME is always ASCII");
let peer_name = [&encoded_peer_id, service_name].join(".");

// allocate with a little extra padding for QNAME encoding
let mut peer_id_bytes = Vec::with_capacity(peer_name.len() + 32);
append_qname(&mut peer_id_bytes, peer_name.as_bytes());

peer_id_bytes
}

/// Appends a `QNAME` (as defined by RFC1035) to the `Vec`.
///
/// # Panic
Expand All @@ -223,7 +252,7 @@ fn append_qname(out: &mut Vec<u8>, name: &[u8]) {
debug_assert!(name.is_ascii());

for element in name.split(|&c| c == b'.') {
assert!(element.len() < 256, "Service name has a label too long");
assert!(element.len() < 64, "Service name has a label too long");
assert_ne!(element.len(), 0, "Service name contains zero length label");
out.push(element.len() as u8);
for chr in element.iter() {
Expand Down Expand Up @@ -367,5 +396,21 @@ mod tests {
assert!(Packet::parse(&query).is_ok());
}

#[test]
fn test_segment_peer_id() {
let str_32 = String::from_utf8(vec![b'x'; 32]).unwrap();
let str_63 = String::from_utf8(vec![b'x'; 63]).unwrap();
let str_64 = String::from_utf8(vec![b'x'; 64]).unwrap();
let str_126 = String::from_utf8(vec![b'x'; 126]).unwrap();
let str_127 = String::from_utf8(vec![b'x'; 127]).unwrap();

assert_eq!(segment_peer_id(str_32.clone()), str_32);
assert_eq!(segment_peer_id(str_63.clone()), str_63);

assert_eq!(segment_peer_id(str_64), [&str_63, "x"].join("."));
assert_eq!(segment_peer_id(str_126), [&str_63, str_63.as_str()].join("."));
assert_eq!(segment_peer_id(str_127), [&str_63, &str_63, "x"].join("."));
}

// TODO: test limits and errors
}
33 changes: 20 additions & 13 deletions misc/mdns/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,18 +411,14 @@ impl<'a> MdnsResponse<'a> {
_ => return None,
};

let peer_name = {
let mut iter = record_value.splitn(2, |c| c == '.');
let name = match iter.next() {
Some(n) => n.to_owned(),
None => return None,
};
if iter.next().map(|v| v.as_bytes()) != Some(SERVICE_NAME) {
return None;
}
name
let mut peer_name = match record_value.rsplitn(4, |c| c == '.').last() {
Some(n) => n.to_owned(),
None => return None,
};

// if we have a segmented name, remove the '.'
peer_name.retain(|c| c != '.');

let peer_id = match data_encoding::BASE32_DNSCURVE.decode(peer_name.as_bytes()) {
Ok(bytes) => match PeerId::from_bytes(bytes) {
Ok(id) => id,
Expand Down Expand Up @@ -541,11 +537,10 @@ mod tests {
use std::{io, time::Duration};
use tokio::{self, prelude::*};
use crate::service::{MdnsPacket, MdnsService};
use multiaddr::multihash::*;

#[test]
fn discover_ourselves() {
fn discover(peer_id: PeerId) {
let mut service = MdnsService::new().unwrap();
let peer_id = PeerId::random();
let stream = stream::poll_fn(move || -> Poll<Option<()>, io::Error> {
loop {
let packet = match service.poll() {
Expand Down Expand Up @@ -575,4 +570,16 @@ mod tests {
.for_each(|_| Ok(())),
);
}

#[test]
fn discover_normal_peer_id() {
discover(PeerId::random())
}

#[test]
fn discover_long_peer_id() {
let max_value = String::from_utf8(vec![b'f'; 42]).unwrap();
let hash = encode(Hash::Identity, max_value.as_ref()).unwrap();
discover(PeerId::from_multihash(hash).unwrap())
}
}