Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ libp2p = { version = "0.56.1", path = "libp2p" }
libp2p-allow-block-list = { version = "0.6.0", path = "misc/allow-block-list" }
libp2p-autonat = { version = "0.15.0", path = "protocols/autonat" }
libp2p-connection-limits = { version = "0.6.0", path = "misc/connection-limits" }
libp2p-core = { version = "0.43.1", path = "core" }
libp2p-core = { version = "0.43.2", path = "core" }
libp2p-dcutr = { version = "0.14.0", path = "protocols/dcutr" }
libp2p-dns = { version = "0.44.0", path = "transports/dns" }
libp2p-floodsub = { version = "0.47.0", path = "protocols/floodsub" }
Expand Down
13 changes: 13 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## 0.43.2

- Add `*_interop` methods to `PeerRecord` for cross-implementation compatibility with Go and JavaScript libp2p.
- `PeerRecord::new_interop()` - Create peer records using standard format
- `PeerRecord::from_signed_envelope_interop()` - Verify peer records using standard format

The standard format uses libp2p-peer-record domain and multicodec identifier (0x0301) for interoperability.
Existing methods (`new()`, `from_signed_envelope()`) maintain backward compatibility with legacy Rust libp2p format.

Use the `*_interop` variants when exchanging peer records with non-Rust libp2p implementations.

See [PR 6230](https://github.com/libp2p/rust-libp2p/pull/6230).

## 0.43.1
- Remove `once_cell` dependency.
See [PR 5913](https://github.com/libp2p/rust-libp2p/pull/5913)
Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "libp2p-core"
edition.workspace = true
rust-version = { workspace = true }
description = "Core traits and structs of libp2p"
version = "0.43.1"
version = "0.43.2"
authors = ["Parity Technologies <[email protected]>"]
license = "MIT"
repository = "https://github.com/libp2p/rust-libp2p"
Expand Down
153 changes: 138 additions & 15 deletions core/src/peer_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@ use web_time::SystemTime;

use crate::{proto, signed_envelope, signed_envelope::SignedEnvelope, DecodeError, Multiaddr};

const PAYLOAD_TYPE: &str = "/libp2p/routing-state-record";
const DOMAIN_SEP: &str = "libp2p-routing-state";
// Legacy constants for backward compatibility with existing Rust libp2p deployments
const LEGACY_PAYLOAD_TYPE: &str = "/libp2p/routing-state-record";
const LEGACY_DOMAIN_SEP: &str = "libp2p-routing-state";

// Standard constants for cross-implementation compatibility with Go/JS libp2p
// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv
// and https://github.com/libp2p/specs/blob/master/RFC/0002-signed-envelopes.md
const STANDARD_PAYLOAD_TYPE: &[u8] = &[0x03, 0x01];
const STANDARD_DOMAIN_SEP: &str = "libp2p-peer-record";

/// Represents a peer routing record.
///
/// Peer records are designed to be distributable and carry a signature by being wrapped in a signed
/// envelope. For more information see RFC0003 of the libp2p specifications: <https://github.com/libp2p/specs/blob/master/RFC/0003-routing-records.md>
///
/// ## Cross-Implementation Compatibility
///
/// This implementation provides two formats:
/// - **Legacy format** (default methods): Compatible with existing Rust libp2p deployments
/// - **Standard format** (`*_interop` methods): Compatible with Go and JavaScript implementations
///
/// Use the `*_interop` variants (e.g., [`PeerRecord::new_interop`], [`PeerRecord::from_signed_envelope_interop`])
/// when you need to exchange peer records with non-Rust libp2p implementations.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PeerRecord {
peer_id: PeerId,
Expand All @@ -25,15 +41,41 @@ pub struct PeerRecord {
}

impl PeerRecord {
/// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`].
/// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`] using legacy format.
///
/// Uses the legacy routing-state-record format for backward compatibility with existing
/// Rust libp2p deployments.
///
/// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid
/// signature and can hence be considered authenticated.
///
/// For cross-implementation compatibility with Go/JS libp2p, use [`Self::from_signed_envelope_interop`].
pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result<Self, FromEnvelopeError> {
Self::from_signed_envelope_impl(envelope, LEGACY_DOMAIN_SEP, LEGACY_PAYLOAD_TYPE.as_bytes())
}

/// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`] using standard interop format.
///
/// Uses the standard libp2p-peer-record format for cross-implementation compatibility
/// with Go and JavaScript libp2p implementations.
///
/// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid
/// signature and can hence be considered authenticated.
pub fn from_signed_envelope_interop(
envelope: SignedEnvelope,
) -> Result<Self, FromEnvelopeError> {
Self::from_signed_envelope_impl(envelope, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE)
}

fn from_signed_envelope_impl(
envelope: SignedEnvelope,
domain: &str,
payload_type: &[u8],
) -> Result<Self, FromEnvelopeError> {
use quick_protobuf::MessageRead;

let (payload, signing_key) =
envelope.payload_and_signing_key(String::from(DOMAIN_SEP), PAYLOAD_TYPE.as_bytes())?;
envelope.payload_and_signing_key(String::from(domain), payload_type)?;
let mut reader = BytesReader::from_bytes(payload);
let record = proto::PeerRecord::from_reader(&mut reader, payload).map_err(DecodeError)?;

Expand All @@ -58,11 +100,41 @@ impl PeerRecord {
})
}

/// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key.
/// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key using legacy format.
///
/// Uses the legacy routing-state-record format for backward compatibility with existing
/// Rust libp2p deployments.
///
/// This is the same key that is used for authenticating every libp2p connection of your
/// application, i.e. what you use when setting up your [`crate::transport::Transport`].
///
/// For cross-implementation compatibility with Go/JS libp2p, use [`Self::new_interop`].
pub fn new(key: &Keypair, addresses: Vec<Multiaddr>) -> Result<Self, SigningError> {
Self::new_impl(
key,
addresses,
LEGACY_DOMAIN_SEP,
LEGACY_PAYLOAD_TYPE.as_bytes(),
)
}

/// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key using standard interop format.
///
/// Uses the standard libp2p-peer-record format for cross-implementation compatibility
/// with Go and JavaScript libp2p implementations.
///
/// This is the same key that is used for authenticating every libp2p connection of your
/// application, i.e. what you use when setting up your [`crate::transport::Transport`].
pub fn new_interop(key: &Keypair, addresses: Vec<Multiaddr>) -> Result<Self, SigningError> {
Self::new_impl(key, addresses, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE)
}

fn new_impl(
key: &Keypair,
addresses: Vec<Multiaddr>,
domain: &str,
payload_type: &[u8],
) -> Result<Self, SigningError> {
use quick_protobuf::MessageWrite;

let seq = SystemTime::now()
Expand Down Expand Up @@ -92,12 +164,8 @@ impl PeerRecord {
buf
};

let envelope = SignedEnvelope::new(
key,
String::from(DOMAIN_SEP),
PAYLOAD_TYPE.as_bytes().to_vec(),
payload,
)?;
let envelope =
SignedEnvelope::new(key, String::from(domain), payload_type.to_vec(), payload)?;

Ok(Self {
peer_id,
Expand Down Expand Up @@ -154,7 +222,7 @@ mod tests {
const HOME: &str = "/ip4/127.0.0.1/tcp/1337";

#[test]
fn roundtrip_envelope() {
fn roundtrip_envelope_legacy() {
let key = Keypair::generate_ed25519();

let record = PeerRecord::new(&key, vec![HOME.parse().unwrap()]).unwrap();
Expand All @@ -166,7 +234,19 @@ mod tests {
}

#[test]
fn mismatched_signature() {
fn roundtrip_envelope_interop() {
let key = Keypair::generate_ed25519();

let record = PeerRecord::new_interop(&key, vec![HOME.parse().unwrap()]).unwrap();

let envelope = record.to_signed_envelope();
let reconstructed = PeerRecord::from_signed_envelope_interop(envelope).unwrap();

assert_eq!(reconstructed, record)
}

#[test]
fn mismatched_signature_legacy() {
use quick_protobuf::MessageWrite;

let addr: Multiaddr = HOME.parse().unwrap();
Expand Down Expand Up @@ -195,8 +275,8 @@ mod tests {

SignedEnvelope::new(
&identity_b,
String::from(DOMAIN_SEP),
PAYLOAD_TYPE.as_bytes().to_vec(),
String::from(LEGACY_DOMAIN_SEP),
LEGACY_PAYLOAD_TYPE.as_bytes().to_vec(),
payload,
)
.unwrap()
Expand All @@ -207,4 +287,47 @@ mod tests {
Err(FromEnvelopeError::MismatchedSignature)
));
}

#[test]
fn mismatched_signature_interop() {
use quick_protobuf::MessageWrite;

let addr: Multiaddr = HOME.parse().unwrap();

let envelope = {
let identity_a = Keypair::generate_ed25519();
let identity_b = Keypair::generate_ed25519();

let payload = {
let record = proto::PeerRecord {
peer_id: identity_a.public().to_peer_id().to_bytes(),
seq: 0,
addresses: vec![proto::AddressInfo {
multiaddr: addr.to_vec(),
}],
};

let mut buf = Vec::with_capacity(record.get_size());
let mut writer = Writer::new(&mut buf);
record
.write_message(&mut writer)
.expect("Encoding to succeed");

buf
};

SignedEnvelope::new(
&identity_b,
String::from(STANDARD_DOMAIN_SEP),
STANDARD_PAYLOAD_TYPE.to_vec(),
payload,
)
.unwrap()
};

assert!(matches!(
PeerRecord::from_signed_envelope_interop(envelope),
Err(FromEnvelopeError::MismatchedSignature)
));
}
}
Loading