Skip to content

Commit fd02c1f

Browse files
kademlia: Preserve publisher & expiration time in DHT records (#162)
This PR fixes a bug with publisher & expiration time not being preserved in DHT records. Resolves #129.
1 parent c6e25eb commit fd02c1f

File tree

6 files changed

+172
-71
lines changed

6 files changed

+172
-71
lines changed

src/protocol/libp2p/kademlia/config.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ pub struct Config {
6565
/// Incoming records validation mode.
6666
pub(super) validation_mode: IncomingRecordValidationMode,
6767

68+
/// Default record TTl.
69+
pub(super) record_ttl: Duration,
70+
6871
/// TX channel for sending events to `KademliaHandle`.
6972
pub(super) event_tx: Sender<KademliaEvent>,
7073

@@ -94,13 +97,14 @@ impl Config {
9497
protocol_names,
9598
update_mode,
9699
validation_mode,
100+
record_ttl,
97101
codec: ProtocolCodec::UnsignedVarint(None),
98102
replication_factor,
99103
known_peers,
100104
cmd_rx,
101105
event_tx,
102106
},
103-
KademliaHandle::new(cmd_tx, event_rx, record_ttl),
107+
KademliaHandle::new(cmd_tx, event_rx),
104108
)
105109
}
106110

src/protocol/libp2p/kademlia/handle.rs

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ use std::{
3131
num::NonZeroUsize,
3232
pin::Pin,
3333
task::{Context, Poll},
34-
time::{Duration, Instant},
3534
};
3635

3736
/// Quorum.
@@ -223,23 +222,15 @@ pub struct KademliaHandle {
223222

224223
/// Next query ID.
225224
next_query_id: usize,
226-
227-
/// Default TTL for the records.
228-
record_ttl: Duration,
229225
}
230226

231227
impl KademliaHandle {
232228
/// Create new [`KademliaHandle`].
233-
pub(super) fn new(
234-
cmd_tx: Sender<KademliaCommand>,
235-
event_rx: Receiver<KademliaEvent>,
236-
record_ttl: Duration,
237-
) -> Self {
229+
pub(super) fn new(cmd_tx: Sender<KademliaCommand>, event_rx: Receiver<KademliaEvent>) -> Self {
238230
Self {
239231
cmd_tx,
240232
event_rx,
241233
next_query_id: 0usize,
242-
record_ttl,
243234
}
244235
}
245236

@@ -265,9 +256,7 @@ impl KademliaHandle {
265256
}
266257

267258
/// Store record to DHT.
268-
pub async fn put_record(&mut self, mut record: Record) -> QueryId {
269-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
270-
259+
pub async fn put_record(&mut self, record: Record) -> QueryId {
271260
let query_id = self.next_query_id();
272261
let _ = self.cmd_tx.send(KademliaCommand::PutRecord { record, query_id }).await;
273262

@@ -277,12 +266,10 @@ impl KademliaHandle {
277266
/// Store record to DHT to the given peers.
278267
pub async fn put_record_to_peers(
279268
&mut self,
280-
mut record: Record,
269+
record: Record,
281270
peers: Vec<PeerId>,
282271
update_local_store: bool,
283272
) -> QueryId {
284-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
285-
286273
let query_id = self.next_query_id();
287274
let _ = self
288275
.cmd_tx
@@ -314,9 +301,7 @@ impl KademliaHandle {
314301

315302
/// Store the record in the local store. Used in combination with
316303
/// [`IncomingRecordValidationMode::Manual`].
317-
pub async fn store_record(&mut self, mut record: Record) {
318-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
319-
304+
pub async fn store_record(&mut self, record: Record) {
320305
let _ = self.cmd_tx.send(KademliaCommand::StoreRecord { record }).await;
321306
}
322307

@@ -337,9 +322,7 @@ impl KademliaHandle {
337322
}
338323

339324
/// Try to initiate `PUT_VALUE` query and if the channel is clogged, return an error.
340-
pub fn try_put_record(&mut self, mut record: Record) -> Result<QueryId, ()> {
341-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
342-
325+
pub fn try_put_record(&mut self, record: Record) -> Result<QueryId, ()> {
343326
let query_id = self.next_query_id();
344327
self.cmd_tx
345328
.try_send(KademliaCommand::PutRecord { record, query_id })
@@ -351,12 +334,10 @@ impl KademliaHandle {
351334
/// return an error.
352335
pub fn try_put_record_to_peers(
353336
&mut self,
354-
mut record: Record,
337+
record: Record,
355338
peers: Vec<PeerId>,
356339
update_local_store: bool,
357340
) -> Result<QueryId, ()> {
358-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
359-
360341
let query_id = self.next_query_id();
361342
self.cmd_tx
362343
.try_send(KademliaCommand::PutRecordToPeers {
@@ -384,9 +365,7 @@ impl KademliaHandle {
384365

385366
/// Try to store the record in the local store, and if the channel is clogged, return an error.
386367
/// Used in combination with [`IncomingRecordValidationMode::Manual`].
387-
pub fn try_store_record(&mut self, mut record: Record) -> Result<(), ()> {
388-
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
389-
368+
pub fn try_store_record(&mut self, record: Record) -> Result<(), ()> {
390369
self.cmd_tx.try_send(KademliaCommand::StoreRecord { record }).map_err(|_| ())
391370
}
392371
}

src/protocol/libp2p/kademlia/message.rs

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
// DEALINGS IN THE SOFTWARE.
2020

21-
use crate::protocol::libp2p::kademlia::{
22-
record::{Key as RecordKey, Record},
23-
schema,
24-
types::KademliaPeer,
21+
use crate::{
22+
protocol::libp2p::kademlia::{
23+
record::{Key as RecordKey, Record},
24+
schema,
25+
types::KademliaPeer,
26+
},
27+
PeerId,
2528
};
2629

2730
use bytes::{Bytes, BytesMut};
2831
use prost::Message;
32+
use std::time::{Duration, Instant};
2933

3034
/// Logging target for the file.
3135
const LOG_TARGET: &str = "litep2p::ipfs::kademlia::message";
@@ -78,16 +82,11 @@ impl KademliaMessage {
7882
}
7983

8084
/// Create `PUT_VALUE` message for `record`.
81-
// TODO: set ttl
8285
pub fn put_value(record: Record) -> Bytes {
8386
let message = schema::kademlia::Message {
8487
key: record.key.clone().into(),
8588
r#type: schema::kademlia::MessageType::PutValue.into(),
86-
record: Some(schema::kademlia::Record {
87-
key: record.key.into(),
88-
value: record.value,
89-
..Default::default()
90-
}),
89+
record: Some(record_to_schema(record)),
9190
cluster_level_raw: 10,
9291
..Default::default()
9392
};
@@ -140,11 +139,7 @@ impl KademliaMessage {
140139
cluster_level_raw: 10,
141140
r#type: schema::kademlia::MessageType::GetValue.into(),
142141
closer_peers: peers.iter().map(|peer| peer.into()).collect(),
143-
record: record.map(|record| schema::kademlia::Record {
144-
key: record.key.to_vec(),
145-
value: record.value,
146-
..Default::default()
147-
}),
142+
record: record.map(record_to_schema),
148143
..Default::default()
149144
};
150145

@@ -174,7 +169,7 @@ impl KademliaMessage {
174169
let record = message.record?;
175170

176171
Some(Self::PutValue {
177-
record: Record::new(record.key, record.value),
172+
record: record_from_schema(record)?,
178173
})
179174
}
180175
1 => {
@@ -185,9 +180,15 @@ impl KademliaMessage {
185180
false => Some(RecordKey::from(message.key.clone())),
186181
};
187182

183+
let record = if let Some(record) = message.record {
184+
Some(record_from_schema(record)?)
185+
} else {
186+
None
187+
};
188+
188189
Some(Self::GetRecord {
189190
key,
190-
record: message.record.map(|record| Record::new(record.key, record.value)),
191+
record,
191192
peers: message
192193
.closer_peers
193194
.iter()
@@ -207,3 +208,82 @@ impl KademliaMessage {
207208
}
208209
}
209210
}
211+
212+
fn record_to_schema(record: Record) -> schema::kademlia::Record {
213+
schema::kademlia::Record {
214+
key: record.key.into(),
215+
value: record.value,
216+
time_received: String::new(),
217+
publisher: record.publisher.map(|peer_id| peer_id.to_bytes()).unwrap_or_default(),
218+
ttl: record
219+
.expires
220+
.map(|expires| {
221+
let now = Instant::now();
222+
if expires > now {
223+
u32::try_from((expires - now).as_secs()).unwrap_or(u32::MAX)
224+
} else {
225+
1 // because 0 means "does not expire"
226+
}
227+
})
228+
.unwrap_or(0),
229+
}
230+
}
231+
232+
fn record_from_schema(record: schema::kademlia::Record) -> Option<Record> {
233+
Some(Record {
234+
key: record.key.into(),
235+
value: record.value,
236+
publisher: if !record.publisher.is_empty() {
237+
Some(PeerId::from_bytes(&record.publisher).ok()?)
238+
} else {
239+
None
240+
},
241+
expires: if record.ttl > 0 {
242+
Some(Instant::now() + Duration::from_secs(record.ttl as u64))
243+
} else {
244+
None
245+
},
246+
})
247+
}
248+
249+
#[cfg(test)]
250+
mod tests {
251+
use super::*;
252+
253+
#[test]
254+
fn non_empty_publisher_and_ttl_are_preserved() {
255+
let expires = Instant::now() + Duration::from_secs(3600);
256+
257+
let record = Record {
258+
key: vec![1, 2, 3].into(),
259+
value: vec![17],
260+
publisher: Some(PeerId::random()),
261+
expires: Some(expires),
262+
};
263+
264+
let got_record = record_from_schema(record_to_schema(record.clone())).unwrap();
265+
266+
assert_eq!(got_record.key, record.key);
267+
assert_eq!(got_record.value, record.value);
268+
assert_eq!(got_record.publisher, record.publisher);
269+
270+
// Check that the expiration time is sane.
271+
let got_expires = got_record.expires.unwrap();
272+
assert!(got_expires - expires >= Duration::ZERO);
273+
assert!(got_expires - expires < Duration::from_secs(10));
274+
}
275+
276+
#[test]
277+
fn empty_publisher_and_ttl_are_preserved() {
278+
let record = Record {
279+
key: vec![1, 2, 3].into(),
280+
value: vec![17],
281+
publisher: None,
282+
expires: None,
283+
};
284+
285+
let got_record = record_from_schema(record_to_schema(record.clone())).unwrap();
286+
287+
assert_eq!(got_record, record);
288+
}
289+
}

src/protocol/libp2p/kademlia/mod.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ use futures::StreamExt;
4545
use multiaddr::Multiaddr;
4646
use tokio::sync::mpsc::{Receiver, Sender};
4747

48-
use std::collections::{hash_map::Entry, HashMap};
48+
use std::{
49+
collections::{hash_map::Entry, HashMap},
50+
time::{Duration, Instant},
51+
};
4952

5053
pub use self::handle::RecordsType;
5154
pub use config::{Config, ConfigBuilder};
@@ -115,7 +118,7 @@ pub(crate) struct Kademlia {
115118
service: TransportService,
116119

117120
/// Local Kademlia key.
118-
_local_key: Key<PeerId>,
121+
local_key: Key<PeerId>,
119122

120123
/// Connected peers,
121124
peers: HashMap<PeerId, PeerContext>,
@@ -147,6 +150,9 @@ pub(crate) struct Kademlia {
147150
/// Incoming records validation mode.
148151
validation_mode: IncomingRecordValidationMode,
149152

153+
/// Default record TTL.
154+
record_ttl: Duration,
155+
150156
/// Query engine.
151157
engine: QueryEngine,
152158

@@ -175,12 +181,13 @@ impl Kademlia {
175181
cmd_rx: config.cmd_rx,
176182
store: MemoryStore::new(),
177183
event_tx: config.event_tx,
178-
_local_key: local_key,
184+
local_key,
179185
pending_dials: HashMap::new(),
180186
executor: QueryExecutor::new(),
181187
pending_substreams: HashMap::new(),
182188
update_mode: config.update_mode,
183189
validation_mode: config.validation_mode,
190+
record_ttl: config.record_ttl,
184191
replication_factor: config.replication_factor,
185192
engine: QueryEngine::new(local_peer_id, config.replication_factor, PARALLELISM_FACTOR),
186193
}
@@ -775,9 +782,15 @@ impl Kademlia {
775782
self.routing_table.closest(Key::from(peer), self.replication_factor).into()
776783
);
777784
}
778-
Some(KademliaCommand::PutRecord { record, query_id }) => {
785+
Some(KademliaCommand::PutRecord { mut record, query_id }) => {
779786
tracing::debug!(target: LOG_TARGET, ?query_id, key = ?record.key, "store record to DHT");
780787

788+
// For `PUT_VALUE` requests originating locally we are always the publisher.
789+
record.publisher = Some(self.local_key.clone().into_preimage());
790+
791+
// Make sure TTL is set.
792+
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
793+
781794
let key = Key::new(record.key.clone());
782795

783796
self.store.put(record.clone());
@@ -788,9 +801,12 @@ impl Kademlia {
788801
self.routing_table.closest(key, self.replication_factor).into(),
789802
);
790803
}
791-
Some(KademliaCommand::PutRecordToPeers { record, query_id, peers, update_local_store }) => {
804+
Some(KademliaCommand::PutRecordToPeers { mut record, query_id, peers, update_local_store }) => {
792805
tracing::debug!(target: LOG_TARGET, ?query_id, key = ?record.key, "store record to DHT to specified peers");
793806

807+
// Make sure TTL is set.
808+
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
809+
794810
if update_local_store {
795811
self.store.put(record.clone());
796812
}
@@ -854,13 +870,16 @@ impl Kademlia {
854870
self.service.add_known_address(&peer, addresses.into_iter());
855871

856872
}
857-
Some(KademliaCommand::StoreRecord { record }) => {
873+
Some(KademliaCommand::StoreRecord { mut record }) => {
858874
tracing::debug!(
859875
target: LOG_TARGET,
860876
key = ?record.key,
861877
"store record in local store",
862878
);
863879

880+
// Make sure TTL is set.
881+
record.expires = record.expires.or_else(|| Some(Instant::now() + self.record_ttl));
882+
864883
self.store.put(record);
865884
}
866885
None => return Err(Error::EssentialTaskClosed),
@@ -914,6 +933,7 @@ mod tests {
914933
replication_factor: 20usize,
915934
update_mode: RoutingTableUpdateMode::Automatic,
916935
validation_mode: IncomingRecordValidationMode::Automatic,
936+
record_ttl: Duration::from_secs(36 * 60 * 60),
917937
event_tx,
918938
cmd_rx,
919939
};

0 commit comments

Comments
 (0)