1use std::collections::HashSet;
15use std::time::Duration;
16
17use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
18
19use crate::codec::{self, CodecError};
20use crate::wire::{Frame, MAX_ENTRIES_PER_PAGE, MAX_HELLO_HASHES};
21
22type Hash = [u8; 32];
23
24pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
29
30pub const MAX_SESSION_ENTRIES: usize = 1_000_000;
37
38fn bounded_inventory(hashes: impl Iterator<Item = Hash>) -> Vec<Hash> {
47 hashes.take(MAX_HELLO_HASHES).collect()
48}
49
50pub trait SyncStore {
53 fn account_id(&self) -> Hash;
56
57 fn snapshot(&self) -> anyhow::Result<Vec<(Hash, Vec<u8>)>>;
63
64 fn ingest(&mut self, signed_bytes: &[u8]) -> anyhow::Result<Ingested>;
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Ingested {
73 Stored,
75 NoChange,
77}
78
79#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
81pub struct SessionReport {
82 pub entries_sent: usize,
83 pub entries_received: usize,
84 pub entries_newly_stored: usize,
85}
86
87#[derive(Debug)]
89pub enum SessionError {
90 Codec(CodecError),
92 Protocol(String),
94 Store(anyhow::Error),
96}
97
98impl std::fmt::Display for SessionError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 SessionError::Codec(e) => write!(f, "sync session transport: {e}"),
102 SessionError::Protocol(m) => write!(f, "sync session protocol violation: {m}"),
103 SessionError::Store(e) => write!(f, "sync session store: {e}"),
104 }
105 }
106}
107
108impl std::error::Error for SessionError {}
109
110pub async fn run_session<S, R, W>(
117 store: &mut S,
118 send: W,
119 recv: R,
120) -> Result<SessionReport, SessionError>
121where
122 S: SyncStore,
123 R: AsyncRead + Unpin,
124 W: AsyncWrite + Unpin,
125{
126 run_session_with_idle_timeout(store, send, recv, DEFAULT_IDLE_TIMEOUT).await
127}
128
129pub async fn run_session_with_idle_timeout<S, R, W>(
132 store: &mut S,
133 mut send: W,
134 mut recv: R,
135 idle_timeout: Duration,
136) -> Result<SessionReport, SessionError>
137where
138 S: SyncStore,
139 R: AsyncRead + Unpin,
140 W: AsyncWrite + Unpin,
141{
142 let account_id = store.account_id();
143 let snapshot = store.snapshot().map_err(SessionError::Store)?;
144 let have = bounded_inventory(snapshot.iter().map(|(h, _)| *h));
145
146 let (peer_have_tx, peer_have_rx) = tokio::sync::oneshot::channel::<HashSet<Hash>>();
149
150 let sender = async move {
151 codec::write_frame(&mut send, &Frame::Hello { account_id, have })
152 .await
153 .map_err(SessionError::Codec)?;
154 let Ok(peer_have) = peer_have_rx.await else {
156 return Ok(0usize);
157 };
158 let mut to_send: Vec<Vec<u8>> = snapshot
159 .into_iter()
160 .filter(|(hash, _)| !peer_have.contains(hash))
161 .map(|(_, bytes)| bytes)
162 .collect();
163 let total = to_send.len();
164 let mut rest = to_send.split_off(0);
166 while !rest.is_empty() {
167 let tail = rest.split_off(rest.len().min(MAX_ENTRIES_PER_PAGE));
168 let page = std::mem::replace(&mut rest, tail);
169 let more = !rest.is_empty();
170 codec::write_frame(&mut send, &Frame::Entries { entries: page, more })
171 .await
172 .map_err(SessionError::Codec)?;
173 }
174 codec::write_frame(&mut send, &Frame::Done).await.map_err(SessionError::Codec)?;
175 send.shutdown().await.map_err(|e| SessionError::Codec(CodecError::Io(e)))?;
179 Ok::<usize, SessionError>(total)
180 };
181
182 let receiver = async {
183 let hello = read_frame_before(&mut recv, idle_timeout).await?;
185 let Frame::Hello { account_id: peer_account, have: peer_have } = hello else {
186 return Err(SessionError::Protocol("peer did not open with a hello".into()));
187 };
188 if peer_account != account_id {
189 return Err(SessionError::Protocol(
190 "peer hello names a different account than this session".into(),
191 ));
192 }
193 let _ = peer_have_tx.send(peer_have.into_iter().collect());
196
197 let mut received = 0usize;
198 let mut newly_stored = 0usize;
199 let mut saw_page = false;
205 let mut saw_final = false;
206 loop {
207 match read_frame_before(&mut recv, idle_timeout).await {
208 Ok(Frame::Entries { entries, more }) => {
209 if saw_final {
212 return Err(SessionError::Protocol(
213 "peer sent an Entries page after the final page".into(),
214 ));
215 }
216 if entries.is_empty() {
220 return Err(SessionError::Protocol(
221 "peer sent an empty Entries page".into(),
222 ));
223 }
224 for bytes in entries {
225 received += 1;
226 if received > MAX_SESSION_ENTRIES {
227 return Err(SessionError::Protocol(format!(
228 "peer streamed more than {MAX_SESSION_ENTRIES} entries",
229 )));
230 }
231 match store.ingest(&bytes).map_err(SessionError::Store)? {
232 Ingested::Stored => newly_stored += 1,
233 Ingested::NoChange => {},
234 }
235 }
236 saw_page = true;
237 saw_final = !more;
238 },
239 Ok(Frame::Done) => {
240 if saw_page && !saw_final {
241 return Err(SessionError::Protocol(
242 "peer sent Done after declaring more pages would follow".into(),
243 ));
244 }
245 break;
246 },
247 Ok(Frame::Hello { .. }) => {
248 return Err(SessionError::Protocol("a second hello mid-session".into()));
249 },
250 Ok(Frame::Auth { .. }) => {
251 return Err(SessionError::Protocol("an auth frame mid-session".into()));
254 },
255 Err(e) => return Err(e),
258 }
259 }
260 Ok::<(usize, usize), SessionError>((received, newly_stored))
261 };
262
263 let (entries_sent, (entries_received, entries_newly_stored)) =
267 tokio::try_join!(sender, receiver)?;
268 Ok(SessionReport { entries_sent, entries_received, entries_newly_stored })
269}
270
271async fn read_frame_before<R: AsyncRead + Unpin>(
275 recv: &mut R,
276 idle_timeout: Duration,
277) -> Result<Frame, SessionError> {
278 match tokio::time::timeout(idle_timeout, codec::read_frame(recv)).await {
279 Ok(Ok(frame)) => Ok(frame),
280 Ok(Err(CodecError::Eof)) => Err(SessionError::Protocol(
281 "peer closed the stream before sending Done — transfer truncated".into(),
282 )),
283 Ok(Err(e)) => Err(SessionError::Codec(e)),
284 Err(_elapsed) => Err(SessionError::Protocol(format!(
285 "peer sent no frame within {idle_timeout:?} — session aborted as idle"
286 ))),
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::collections::HashMap;
293
294 use super::*;
295
296 struct MemStore {
300 account: Hash,
301 entries: HashMap<Hash, Vec<u8>>,
302 }
303
304 impl MemStore {
305 fn new(account: Hash, entries: &[(Hash, Vec<u8>)]) -> Self {
306 Self { account, entries: entries.iter().cloned().collect() }
307 }
308 }
309
310 impl SyncStore for MemStore {
311 fn account_id(&self) -> Hash {
312 self.account
313 }
314 fn snapshot(&self) -> anyhow::Result<Vec<(Hash, Vec<u8>)>> {
315 let mut v: Vec<_> = self.entries.iter().map(|(h, b)| (*h, b.clone())).collect();
316 v.sort_by_key(|(h, _)| *h);
317 Ok(v)
318 }
319 fn ingest(&mut self, signed_bytes: &[u8]) -> anyhow::Result<Ingested> {
320 let hash: Hash = signed_bytes[..32].try_into().unwrap();
322 match self.entries.entry(hash) {
323 std::collections::hash_map::Entry::Occupied(_) => Ok(Ingested::NoChange),
324 std::collections::hash_map::Entry::Vacant(slot) => {
325 slot.insert(signed_bytes.to_vec());
326 Ok(Ingested::Stored)
327 },
328 }
329 }
330 }
331
332 fn entry(seed: u8) -> (Hash, Vec<u8>) {
333 let mut bytes = vec![seed; 40];
334 bytes[..32].copy_from_slice(&[seed; 32]);
335 ([seed; 32], bytes)
336 }
337
338 async fn sync_pair(a: &mut MemStore, b: &mut MemStore) -> (SessionReport, SessionReport) {
339 let (a_send, b_recv) = tokio::io::duplex(1 << 20);
340 let (b_send, a_recv) = tokio::io::duplex(1 << 20);
341 let (ra, rb) =
342 tokio::join!(run_session(a, a_send, a_recv), run_session(b, b_send, b_recv),);
343 (ra.unwrap(), rb.unwrap())
344 }
345
346 #[tokio::test]
347 async fn a_peer_with_nothing_restores_the_full_set_from_the_other() {
348 let full: Vec<_> = (0u8..5).map(entry).collect();
349 let mut a = MemStore::new([0xac; 32], &full);
350 let mut b = MemStore::new([0xac; 32], &[]);
351 let (ra, rb) = sync_pair(&mut a, &mut b).await;
352
353 assert_eq!(ra.entries_sent, 5, "the full peer sends all five");
354 assert_eq!(rb.entries_newly_stored, 5, "the empty peer stores all five");
355 assert_eq!(a.entries.len(), 5, "the full peer is unchanged");
356 assert_eq!(b.entries.len(), 5, "the empty peer is now complete");
357 assert_eq!(a.entries, b.entries, "both hold the same set — restore-from-peer");
358 }
359
360 #[tokio::test]
361 async fn disjoint_peers_converge_to_the_union_both_directions() {
362 let mut a = MemStore::new([1; 32], &[entry(1), entry(2), entry(3)]);
363 let mut b = MemStore::new([1; 32], &[entry(3), entry(4), entry(5)]);
364 let (ra, rb) = sync_pair(&mut a, &mut b).await;
365
366 assert_eq!(rb.entries_newly_stored, 2, "b gains 1 and 2");
369 assert_eq!(ra.entries_newly_stored, 2, "a gains 4 and 5");
370 let union: HashSet<Hash> = (1u8..=5).map(|s| [s; 32]).collect();
371 assert_eq!(a.entries.keys().copied().collect::<HashSet<_>>(), union);
372 assert_eq!(b.entries.keys().copied().collect::<HashSet<_>>(), union);
373 }
374
375 #[tokio::test]
376 async fn already_in_sync_transfers_nothing() {
377 let same: Vec<_> = (10u8..13).map(entry).collect();
378 let mut a = MemStore::new([2; 32], &same);
379 let mut b = MemStore::new([2; 32], &same);
380 let (ra, rb) = sync_pair(&mut a, &mut b).await;
381 assert_eq!(ra.entries_sent, 0);
382 assert_eq!(rb.entries_sent, 0);
383 assert_eq!(ra.entries_newly_stored, 0);
384 assert_eq!(rb.entries_newly_stored, 0);
385 }
386
387 #[tokio::test]
390 async fn a_truncated_transfer_fails_rather_than_reporting_success() {
391 use crate::codec::write_frame;
392 let mut receiver = MemStore::new([5; 32], &[]);
393 let (mut peer_send, recv) = tokio::io::duplex(1 << 16);
395 let (send, _peer_recv) = tokio::io::duplex(1 << 16);
396 let feeder = tokio::spawn(async move {
397 write_frame(&mut peer_send, &Frame::Hello { account_id: [5; 32], have: vec![] })
398 .await
399 .unwrap();
400 write_frame(&mut peer_send, &Frame::Entries {
401 entries: vec![entry(7).1],
402 more: true, })
404 .await
405 .unwrap();
406 });
408 let result = run_session(&mut receiver, send, recv).await;
409 feeder.await.unwrap();
410 assert!(
411 matches!(result, Err(SessionError::Protocol(_))),
412 "EOF before Done is a truncated transfer, not success: {result:?}",
413 );
414 }
415
416 #[tokio::test]
419 async fn done_after_a_more_true_page_is_rejected() {
420 use crate::codec::write_frame;
421 let mut receiver = MemStore::new([6; 32], &[]);
422 let (mut peer_send, recv) = tokio::io::duplex(1 << 16);
423 let (send, _peer_recv) = tokio::io::duplex(1 << 16);
424 let feeder = tokio::spawn(async move {
425 write_frame(&mut peer_send, &Frame::Hello { account_id: [6; 32], have: vec![] })
426 .await
427 .unwrap();
428 write_frame(&mut peer_send, &Frame::Entries { entries: vec![entry(1).1], more: true })
429 .await
430 .unwrap();
431 write_frame(&mut peer_send, &Frame::Done).await.unwrap();
432 });
433 let result = run_session(&mut receiver, send, recv).await;
434 feeder.await.unwrap();
435 assert!(
436 matches!(result, Err(SessionError::Protocol(_))),
437 "Done after more:true is a declared-incomplete transfer: {result:?}",
438 );
439 }
440
441 #[tokio::test]
446 async fn a_silent_peer_times_out() {
447 use crate::codec::write_frame;
448 let mut receiver = MemStore::new([11; 32], &[]);
449 let (mut peer_send, recv) = tokio::io::duplex(1 << 16);
453 let (send, _peer_recv) = tokio::io::duplex(1 << 16);
454 write_frame(&mut peer_send, &Frame::Hello { account_id: [11; 32], have: vec![] })
455 .await
456 .unwrap();
457 let result = run_session_with_idle_timeout(
458 &mut receiver,
459 send,
460 recv,
461 std::time::Duration::from_millis(50),
462 )
463 .await;
464 drop(peer_send); match result {
466 Err(SessionError::Protocol(m)) => assert!(m.contains("idle"), "{m}"),
467 other => panic!("expected an idle-timeout abort: {other:?}"),
468 }
469 }
470
471 #[tokio::test]
473 async fn a_page_after_the_final_page_is_rejected() {
474 use crate::codec::write_frame;
475 let mut receiver = MemStore::new([12; 32], &[]);
476 let (mut peer_send, recv) = tokio::io::duplex(1 << 16);
477 let (send, _peer_recv) = tokio::io::duplex(1 << 16);
478 let feeder = tokio::spawn(async move {
479 write_frame(&mut peer_send, &Frame::Hello { account_id: [12; 32], have: vec![] })
480 .await
481 .unwrap();
482 write_frame(&mut peer_send, &Frame::Entries { entries: vec![entry(1).1], more: false })
483 .await
484 .unwrap();
485 write_frame(&mut peer_send, &Frame::Entries { entries: vec![entry(2).1], more: false })
487 .await
488 .unwrap();
489 });
490 let result = run_session(&mut receiver, send, recv).await;
491 feeder.await.unwrap();
492 match result {
493 Err(SessionError::Protocol(m)) => assert!(m.contains("after the final page"), "{m}"),
494 other => panic!("expected the after-final-page guard: {other:?}"),
495 }
496 }
497
498 #[tokio::test]
499 async fn an_empty_entries_page_is_rejected() {
500 use crate::codec::write_frame;
501 let mut receiver = MemStore::new([8; 32], &[]);
502 let (mut peer_send, recv) = tokio::io::duplex(1 << 16);
503 let (send, _peer_recv) = tokio::io::duplex(1 << 16);
504 let feeder = tokio::spawn(async move {
505 write_frame(&mut peer_send, &Frame::Hello { account_id: [8; 32], have: vec![] })
506 .await
507 .unwrap();
508 write_frame(&mut peer_send, &Frame::Entries { entries: vec![], more: true })
509 .await
510 .unwrap();
511 });
512 let result = run_session(&mut receiver, send, recv).await;
513 feeder.await.unwrap();
514 match result {
517 Err(SessionError::Protocol(m)) => assert!(m.contains("empty Entries page"), "{m}"),
518 other => panic!("expected the empty-page guard: {other:?}"),
519 }
520 }
521
522 #[test]
523 fn the_outgoing_inventory_is_capped_to_the_wire_limit() {
524 let over = MAX_HELLO_HASHES + 100;
525 let hashes = (0..over).map(|i| {
526 let mut h = [0u8; 32];
527 h[..8].copy_from_slice(&(i as u64).to_be_bytes());
528 h
529 });
530 let bounded = bounded_inventory(hashes);
531 assert_eq!(bounded.len(), MAX_HELLO_HASHES, "never advertises more than the peer decodes");
532 let frame = Frame::Hello { account_id: [0; 32], have: bounded };
534 assert!(Frame::decode(&frame.encode()).is_ok());
535 }
536
537 #[tokio::test]
538 async fn a_mismatched_account_aborts_the_session() {
539 let mut a = MemStore::new([1; 32], &[entry(1)]);
540 let mut b = MemStore::new([2; 32], &[entry(2)]);
541 let (a_send, b_recv) = tokio::io::duplex(1 << 16);
542 let (b_send, a_recv) = tokio::io::duplex(1 << 16);
543 let (ra, rb) =
544 tokio::join!(run_session(&mut a, a_send, a_recv), run_session(&mut b, b_send, b_recv),);
545 assert!(matches!(ra, Err(SessionError::Protocol(_))));
546 assert!(matches!(rb, Err(SessionError::Protocol(_))));
547 }
548}