-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathpeer_record.rs
More file actions
338 lines (284 loc) · 11.5 KB
/
peer_record.rs
File metadata and controls
338 lines (284 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
use libp2p_identity::{Keypair, PeerId, SigningError};
use quick_protobuf::{BytesReader, Writer};
use web_time::SystemTime;
use crate::{proto, signed_envelope, signed_envelope::SignedEnvelope, DecodeError, Multiaddr};
// 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,
seq: u64,
addresses: Vec<Multiaddr>,
/// A signed envelope representing this [`PeerRecord`].
///
/// If this [`PeerRecord`] was constructed from a [`SignedEnvelope`], this is the original
/// instance.
envelope: SignedEnvelope,
}
impl PeerRecord {
/// 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), payload_type)?;
let mut reader = BytesReader::from_bytes(payload);
let record = proto::PeerRecord::from_reader(&mut reader, payload).map_err(DecodeError)?;
let peer_id = PeerId::from_bytes(&record.peer_id)?;
if peer_id != signing_key.to_peer_id() {
return Err(FromEnvelopeError::MismatchedSignature);
}
let seq = record.seq;
let addresses = record
.addresses
.into_iter()
.map(|a| a.multiaddr.to_vec().try_into())
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
peer_id,
seq,
addresses,
envelope,
})
}
/// 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()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("now() is never before UNIX_EPOCH")
.as_secs();
let peer_id = key.public().to_peer_id();
let payload = {
let record = proto::PeerRecord {
peer_id: peer_id.to_bytes(),
seq,
addresses: addresses
.iter()
.map(|m| proto::AddressInfo {
multiaddr: m.to_vec(),
})
.collect(),
};
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
};
let envelope =
SignedEnvelope::new(key, String::from(domain), payload_type.to_vec(), payload)?;
Ok(Self {
peer_id,
seq,
addresses,
envelope,
})
}
pub fn to_signed_envelope(&self) -> SignedEnvelope {
self.envelope.clone()
}
pub fn into_signed_envelope(self) -> SignedEnvelope {
self.envelope
}
pub fn peer_id(&self) -> PeerId {
self.peer_id
}
pub fn seq(&self) -> u64 {
self.seq
}
pub fn addresses(&self) -> &[Multiaddr] {
self.addresses.as_slice()
}
}
#[derive(thiserror::Error, Debug)]
pub enum FromEnvelopeError {
/// Failed to extract the payload from the envelope.
#[error("Failed to extract payload from envelope")]
BadPayload(#[from] signed_envelope::ReadPayloadError),
/// Failed to decode the provided bytes as a [`PeerRecord`].
#[error("Failed to decode bytes as PeerRecord")]
InvalidPeerRecord(#[from] DecodeError),
/// Failed to decode the peer ID.
#[error("Failed to decode bytes as PeerId")]
InvalidPeerId(#[from] libp2p_identity::ParseError),
/// The signer of the envelope is different than the peer id in the record.
#[error("The signer of the envelope is different than the peer id in the record")]
MismatchedSignature,
/// Failed to decode a multi-address.
#[error("Failed to decode bytes as MultiAddress")]
InvalidMultiaddr(#[from] multiaddr::Error),
}
#[cfg(test)]
mod tests {
use super::*;
const HOME: &str = "/ip4/127.0.0.1/tcp/1337";
#[test]
fn roundtrip_envelope_legacy() {
let key = Keypair::generate_ed25519();
let record = PeerRecord::new(&key, vec![HOME.parse().unwrap()]).unwrap();
let envelope = record.to_signed_envelope();
let reconstructed = PeerRecord::from_signed_envelope(envelope).unwrap();
assert_eq!(reconstructed, record)
}
#[test]
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();
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(LEGACY_DOMAIN_SEP),
LEGACY_PAYLOAD_TYPE.as_bytes().to_vec(),
payload,
)
.unwrap()
};
assert!(matches!(
PeerRecord::from_signed_envelope(envelope),
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)
));
}
}