diff --git a/Cargo.lock b/Cargo.lock index fdeada37500..00ecc7f56f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2519,7 +2519,7 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.43.1" +version = "0.43.2" dependencies = [ "either", "fnv", diff --git a/Cargo.toml b/Cargo.toml index 9e65d1ab62f..db4ac657407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 90d26e75608..45aa3a01a88 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -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) diff --git a/core/Cargo.toml b/core/Cargo.toml index 8c6018adb16..a151b8691f2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -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 "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/core/src/peer_record.rs b/core/src/peer_record.rs index 9c6b7f73f05..067bbc05e0f 100644 --- a/core/src/peer_record.rs +++ b/core/src/peer_record.rs @@ -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: +/// +/// ## 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, @@ -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::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::from_signed_envelope_impl(envelope, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE) + } + + fn from_signed_envelope_impl( + envelope: SignedEnvelope, + domain: &str, + payload_type: &[u8], + ) -> Result { 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)?; @@ -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) -> Result { + 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) -> Result { + Self::new_impl(key, addresses, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE) + } + + fn new_impl( + key: &Keypair, + addresses: Vec, + domain: &str, + payload_type: &[u8], + ) -> Result { use quick_protobuf::MessageWrite; let seq = SystemTime::now() @@ -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, @@ -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(); @@ -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(); @@ -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() @@ -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) + )); + } }