Skip to main content

aranya_runtime/sync/
responder.rs

1use buggy::{BugExt as _, bug};
2use heapless::Vec;
3use serde::{Deserialize, Serialize};
4
5use super::{
6    COMMAND_RESPONSE_MAX, COMMAND_SAMPLE_MAX, MAX_SYNC_MESSAGE_SIZE, PEER_HEAD_MAX, PollIncoming,
7    SEGMENT_BUFFER_MAX, SyncError,
8    requester::SyncRequestMessage,
9    wire::{CommandMeta, SyncType},
10};
11use crate::{
12    LocatedAddress, Prior, StorageError,
13    command::{Address, CmdId, Command as _},
14    storage::{
15        GraphId, Location, MaxCut, Segment as _, Storage, StorageProvider, TraversalBuffer,
16        TraversalBuffers,
17    },
18};
19
20#[derive(Default, Debug)]
21pub struct PeerCache {
22    heads: Vec<LocatedAddress, { PEER_HEAD_MAX }>,
23}
24
25impl PeerCache {
26    pub const fn new() -> Self {
27        Self { heads: Vec::new() }
28    }
29
30    pub fn heads(&self) -> &[LocatedAddress] {
31        &self.heads
32    }
33
34    pub fn add_command<S>(
35        &mut self,
36        storage: &S,
37        new: LocatedAddress,
38        buffer: &mut TraversalBuffer,
39    ) -> Result<(), StorageError>
40    where
41        S: Storage,
42    {
43        let mut add_command = true;
44
45        let mut retain_head = |old: &LocatedAddress| -> Result<bool, StorageError> {
46            if old.id == new.id || storage.is_ancestor(new.location(), old.location(), buffer)? {
47                // Don't add this command, keep existing command
48                add_command = false;
49                return Ok(true);
50            }
51            if storage.is_ancestor(old.location(), new.location(), buffer)? {
52                // Remove existing head.
53                return Ok(false);
54            }
55            // Just keep existing head.
56            Ok(true)
57        };
58        self.heads.retain(|h| retain_head(h).unwrap_or(false));
59        if add_command {
60            // TODO(jdygert): Replace an old head when full?
61            self.heads.push(new).ok();
62        }
63
64        Ok(())
65    }
66}
67
68// TODO: Use compile-time args. This initial definition results in this clippy warning:
69// https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant.
70// As the buffer consts will be compile-time variables in the future, we will be
71// able to tune these buffers for smaller footprints. Right now, this enum is not
72// suitable for small devices (`SyncResponse` is 14448 bytes).
73/// Messages sent from the responder to the requester.
74#[derive(Serialize, Deserialize, Debug)]
75#[allow(clippy::large_enum_variant)]
76pub(crate) enum SyncResponseMessage {
77    /// Sent in response to a `SyncRequest`
78    SyncResponse {
79        /// A random-value produced by a cryptographically secure RNG.
80        session_id: u128,
81        /// If the responder intends to send a value of command bytes
82        /// greater than the responder's configured maximum, the responder
83        /// will send more than one `SyncResponse`. The first message has an
84        /// index of 1, and each following is incremented.
85        response_index: u64,
86        /// Commands that the responder believes the requester does not have.
87        commands: Vec<CommandMeta, COMMAND_RESPONSE_MAX>,
88    },
89
90    /// End a sync session if `SyncRequest.max_bytes` has been reached or
91    /// there are no remaining commands to send.
92    SyncEnd {
93        /// A random-value produced by a cryptographically secure RNG
94        /// corresponding to the `session_id` in the initial `SyncRequest`.
95        session_id: u128,
96        /// Largest index of any `SyncResponse`
97        max_index: u64,
98        /// Set `true` if this message was sent due to reaching the `max_bytes`
99        /// budget.
100        remaining: bool,
101    },
102
103    /// Message sent by a responder after a sync has been completed, but before
104    /// the session has ended, if it has new commands in it's graph. If a
105    /// requester wishes to respond to this message, it should do so with a
106    /// new `SyncRequest`. This message may use the existing `session_id`.
107    Offer {
108        /// A random-value produced by a cryptographically secure RNG
109        /// corresponding to the `session_id` in the initial `SyncRequest`.
110        session_id: u128,
111        /// Head of the branch the responder wishes to send.
112        head: CmdId,
113    },
114
115    /// Message sent by either requester or responder to indicate the session
116    /// has been terminated or the `session_id` is no longer valid.
117    EndSession { session_id: u128 },
118}
119
120impl SyncResponseMessage {
121    pub(crate) fn session_id(&self) -> u128 {
122        match self {
123            Self::SyncResponse { session_id, .. } => *session_id,
124            Self::SyncEnd { session_id, .. } => *session_id,
125            Self::Offer { session_id, .. } => *session_id,
126            Self::EndSession { session_id, .. } => *session_id,
127        }
128    }
129}
130
131#[derive(Debug, Default)]
132enum SyncResponderState {
133    #[default]
134    New,
135    Start,
136    Send,
137    Idle,
138    Reset,
139    Stopped,
140}
141
142pub struct SyncResponder {
143    session_id: Option<u128>,
144    graph_id: Option<GraphId>,
145    state: SyncResponderState,
146    bytes_sent: u64,
147    next_send: usize,
148    message_index: usize,
149    has: Vec<Address, COMMAND_SAMPLE_MAX>,
150    to_send: Vec<Location, SEGMENT_BUFFER_MAX>,
151}
152
153impl Default for SyncResponder {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159/// Insert `loc` into a bounded vec that keeps the lowest `max_cut`
160/// entries. If full, replaces the highest `max_cut` entry when the
161/// new one is lower.
162fn push_bounded(v: &mut Vec<Location, SEGMENT_BUFFER_MAX>, loc: Location) {
163    if v.push(loc).is_err() {
164        // Full — find the entry with the highest max_cut.
165        let (max_idx, _) = v
166            .iter()
167            .enumerate()
168            .max_by_key(|(_, l)| l.max_cut)
169            .expect("non-empty");
170        if loc.max_cut < v[max_idx].max_cut {
171            v[max_idx] = loc;
172        }
173    }
174}
175
176/// Walk backwards from `head` along the skip list, taking any skip entry
177/// that stays at or above `target`, until the next step would drop below
178/// `target`. Returns the segment we stopped at — the entry point for the
179/// per-round traversal in `find_needed_segments`.
180fn skip_jump<S: Storage>(
181    storage: &S,
182    head: Location,
183    target: MaxCut,
184) -> Result<Location, StorageError> {
185    if head.max_cut <= target {
186        return Ok(head);
187    }
188    let mut current = head;
189    loop {
190        let seg = storage.get_segment(current)?;
191
192        // Smallest skip entry at or above target (and below current).
193        let best = seg
194            .skip_list()
195            .iter()
196            .copied()
197            .filter(|s| s.max_cut >= target && s.max_cut < current.max_cut)
198            .min_by_key(|s| s.max_cut);
199        if let Some(skip) = best {
200            current = skip;
201            continue;
202        }
203
204        // No useful skip. If any prior would drop below target, stop here.
205        let prior_below = match seg.prior() {
206            Prior::Single(p) => p.max_cut < target,
207            Prior::Merge(a, b) => a.max_cut < target || b.max_cut < target,
208            Prior::None => true,
209        };
210        if prior_below {
211            return Ok(current);
212        }
213
214        match seg.prior() {
215            Prior::Single(p) => current = p,
216            _ => return Ok(current),
217        }
218    }
219}
220
221impl SyncResponder {
222    /// Create a new [`SyncResponder`].
223    pub const fn new() -> Self {
224        Self {
225            session_id: None,
226            graph_id: None,
227            state: SyncResponderState::New,
228            bytes_sent: 0,
229            next_send: 0,
230            message_index: 0,
231            has: Vec::new(),
232            to_send: Vec::new(),
233        }
234    }
235
236    /// Returns true if [`Self::poll`] would produce a message.
237    pub fn ready(&self) -> bool {
238        use SyncResponderState::*;
239        match self.state {
240            Reset | Start | Send => true, // TODO(chip): For Send, check whether to_send has anything to send
241            New | Idle | Stopped => false,
242        }
243    }
244
245    /// Write a sync message in to the target buffer. Returns the number
246    /// of bytes written.
247    pub fn poll(
248        &mut self,
249        target: &mut [u8],
250        provider: &mut impl StorageProvider,
251        response_cache: &mut PeerCache,
252        buffers: &mut TraversalBuffers,
253    ) -> Result<usize, SyncError> {
254        // TODO(chip): return a status enum instead of usize
255        use SyncResponderState as S;
256        let length = match self.state {
257            S::New | S::Idle | S::Stopped => {
258                return Err(SyncError::NotReady); // TODO(chip): return Ok(NotReady)
259            }
260            S::Start => {
261                let Some(graph_id) = self.graph_id else {
262                    self.state = S::Reset;
263                    bug!("poll called before graph_id was set");
264                };
265
266                let storage = match provider.get_storage(graph_id) {
267                    Ok(s) => s,
268                    Err(e) => {
269                        self.state = S::Reset;
270                        return Err(e.into());
271                    }
272                };
273
274                self.state = S::Send;
275                for command in &self.has {
276                    // We only need to check commands that are a part of our graph.
277                    if let Some(cmd_loc) = storage.get_location(*command, &mut buffers.primary)? {
278                        response_cache.add_command(
279                            storage,
280                            LocatedAddress {
281                                id: command.id,
282                                segment: cmd_loc.segment,
283                                max_cut: command.max_cut,
284                            },
285                            &mut buffers.primary,
286                        )?;
287                    }
288                }
289                self.to_send = Self::find_needed_segments(&self.has, storage, buffers)?;
290
291                self.get_next(target, provider)?
292            }
293            S::Send => self.get_next(target, provider)?,
294            S::Reset => {
295                self.state = S::Stopped;
296                let message = SyncResponseMessage::EndSession {
297                    session_id: self.session_id()?,
298                };
299                Self::write(target, message)?
300            }
301        };
302
303        Ok(length)
304    }
305
306    /// Receive a sync poll. Updates the responder's state for later polling.
307    pub fn receive(&mut self, poll: PollIncoming) -> Result<(), SyncError> {
308        self.dispatch(poll.message)
309    }
310
311    /// Begin a new session in-process, without going through the wire.
312    ///
313    /// Used by transports that need to push out an unsolicited update to a
314    /// subscribed peer: instead of round-tripping a `SyncRequest` through the
315    /// network, the transport seeds the responder directly with the heads it
316    /// already knows about.
317    pub fn start_session(
318        &mut self,
319        session_id: u128,
320        graph_id: GraphId,
321        max_bytes: u64,
322        heads: impl IntoIterator<Item = Address>,
323    ) -> Result<(), SyncError> {
324        let mut commands: Vec<Address, COMMAND_SAMPLE_MAX> = Vec::new();
325        heads
326            .into_iter()
327            .try_for_each(|head| commands.push(head).ok())
328            .ok_or(SyncError::CommandOverflow)?;
329        self.dispatch(SyncRequestMessage::SyncRequest {
330            session_id,
331            graph_id,
332            max_bytes,
333            commands,
334        })
335    }
336
337    fn dispatch(&mut self, message: SyncRequestMessage) -> Result<(), SyncError> {
338        if self.session_id.is_none() {
339            self.session_id = Some(message.session_id());
340        }
341        if self.session_id != Some(message.session_id()) {
342            return Err(SyncError::SessionMismatch);
343        }
344
345        match message {
346            SyncRequestMessage::SyncRequest {
347                graph_id,
348                max_bytes,
349                commands,
350                ..
351            } => {
352                self.state = SyncResponderState::Start;
353                self.graph_id = Some(graph_id);
354                self.bytes_sent = max_bytes;
355                self.to_send = Vec::new();
356                self.has = commands;
357                self.next_send = 0;
358                return Ok(());
359            }
360            SyncRequestMessage::RequestMissing { .. } => {
361                todo!()
362            }
363            SyncRequestMessage::SyncResume { .. } => {
364                todo!()
365            }
366            SyncRequestMessage::EndSession { .. } => {
367                self.state = SyncResponderState::Stopped;
368            }
369        }
370
371        Ok(())
372    }
373
374    fn write_sync_type(target: &mut [u8], msg: SyncType) -> Result<usize, SyncError> {
375        Ok(postcard::to_slice(&msg, target)?.len())
376    }
377
378    fn write(target: &mut [u8], msg: SyncResponseMessage) -> Result<usize, SyncError> {
379        Ok(postcard::to_slice(&msg, target)?.len())
380    }
381
382    /// Returns segments (or partial segments) that the peer doesn't have.
383    ///
384    /// Uses a single backward traversal with coverage propagation to
385    /// eliminate all `is_ancestor()` calls. See
386    /// `aranya-docs/docs/find-needed-segments-optimization.md`.
387    fn find_needed_segments(
388        commands: &[Address],
389        storage: &impl Storage,
390        buffers: &mut TraversalBuffers,
391    ) -> Result<Vec<Location, SEGMENT_BUFFER_MAX>, SyncError> {
392        // Resolve command addresses to locations. Use buffers.primary as
393        // scratch for each get_location call (it gets cleared before main loop).
394        if commands.len() > COMMAND_SAMPLE_MAX {
395            bug!(
396                "commands length {} exceeds COMMAND_SAMPLE_MAX",
397                commands.len()
398            );
399        }
400        let mut have_locations: Vec<Location, COMMAND_SAMPLE_MAX> = Vec::new();
401        for &addr in commands {
402            if let Some(location) = storage.get_location(addr, &mut buffers.primary)? {
403                let _ = have_locations.push(location);
404            }
405        }
406
407        // Sort descending by max_cut so we can discard from the front as we
408        // descend through the graph.
409        have_locations.sort_by_key(|loc| core::cmp::Reverse(loc.max_cut));
410
411        // Index into have_locations: everything before this has max_cut above
412        // the current segment's longest_max_cut and can be skipped.
413        let mut have_cursor: usize = 0;
414
415        // heads queue: segments to process, popped by highest max_cut.
416        let heads = buffers.primary.get();
417
418        // Jump from head toward highest_have + SEGMENT_BUFFER_MAX before
419        // starting the main traversal, so per-round cost is O(log n) instead
420        // of O(n).
421        let head = storage.get_head()?;
422        let highest_have = have_locations
423            .first()
424            .map(|l| l.max_cut)
425            .unwrap_or(MaxCut::new(0));
426        let skip_target = highest_have
427            .checked_add(SEGMENT_BUFFER_MAX as u64)
428            .assume("skip target overflow")?;
429        let start = skip_jump(storage, head, skip_target)?;
430        heads.push(start)?;
431
432        // pending queue: segments tentatively needed by the peer.
433        let pending = buffers.secondary.get();
434
435        // Accumulate needed segments, keeping only the SEGMENT_BUFFER_MAX
436        // entries with the lowest max_cut (ancestors first). When full,
437        // the highest max_cut entry is replaced if the new one is lower.
438        let mut collected: Vec<Location, SEGMENT_BUFFER_MAX> = Vec::new();
439        let mut prev_max_cut: Option<MaxCut> = None;
440
441        while let Some((head, covered)) = heads.pop_covered()? {
442            // Flush pending entries whose shortest_max_cut (stored as max_cut)
443            // is above the just-popped entry's longest_max_cut. No future
444            // have_location can reach them since we process in descending order.
445            if prev_max_cut != Some(head.max_cut) {
446                pending.drain_above(head.max_cut, |loc| push_bounded(&mut collected, loc))?;
447                prev_max_cut = Some(head.max_cut);
448            }
449
450            let segment = storage.get_segment(head)?;
451
452            if covered {
453                // Case 1: Covered — the peer has this segment up to
454                // head.max_cut. Update pending to reflect partial or full
455                // coverage so we don't send what the peer already has.
456                let longest = segment.longest_max_cut()?;
457                pending.cover_up_to(head.segment, head.max_cut, longest)?;
458                // Propagate coverage to priors so they'll be processed as
459                // covered if not yet visited.
460                for prior in segment.prior() {
461                    heads.push_covered(prior, true)?;
462                }
463                // Early termination: if all remaining heads are covered, stop.
464                // Every remaining path leads to segments the peer already has.
465                if heads.all_covered() && !heads.is_empty() {
466                    break;
467                }
468                continue;
469            }
470
471            // Advance have_cursor past locations with max_cut above this
472            // segment's longest_max_cut — they've already been passed.
473            let longest = segment.longest_max_cut()?;
474            while have_locations
475                .get(have_cursor)
476                .is_some_and(|h| h.max_cut > longest)
477            {
478                have_cursor = have_cursor
479                    .checked_add(1)
480                    .assume("index must not overflow")?;
481            }
482
483            // Look for a have_location in this segment: same SegmentIndex
484            // with max_cut within shortest_max_cut..=longest_max_cut.
485            let shortest = segment.shortest_max_cut();
486            let mut best_have: Option<(usize, Location)> = None;
487            for scan in have_cursor..have_locations.len() {
488                let hloc = have_locations[scan];
489                if hloc.max_cut < shortest {
490                    break; // rest are even lower, can't be in this segment
491                }
492                if hloc.segment == head.segment {
493                    best_have = Some((scan, hloc));
494                    break; // sorted in descending order, so first match is the highest max_cut
495                }
496            }
497
498            if let Some((_idx, hloc)) = best_have {
499                // Case 2: Contains a have_location. Push priors as
500                // covered — the peer has at least part of this segment,
501                // so its priors are reachable.
502                for prior in segment.prior() {
503                    heads.push_covered(prior, true)?;
504                }
505
506                // If the peer doesn't have the whole segment (have_location
507                // is not at the segment head), add a partial entry to pending
508                // starting from the command after the highest have_location.
509                if hloc.max_cut < longest {
510                    let next_max_cut = hloc
511                        .max_cut
512                        .checked_add(1)
513                        .assume("command + 1 mustn't overflow")?;
514                    let partial_loc = Location {
515                        max_cut: next_max_cut,
516                        segment: head.segment,
517                    };
518                    pending.push(partial_loc)?;
519                }
520                // else: peer has the entire segment, nothing to send.
521            } else {
522                // Case 3: Uncovered, no have_location. Add to pending and
523                // continue traversal through priors.
524                pending.push(segment.first_location())?;
525                for prior in segment.prior() {
526                    heads.push(prior)?;
527                }
528            }
529
530            // Early termination: if all remaining heads are covered, stop.
531            // Every remaining path leads to segments the peer already has.
532            if heads.all_covered() && !heads.is_empty() {
533                break;
534            }
535        }
536
537        // Flush remaining uncovered pending segments. Covered entries
538        // are discarded — the peer already has them.
539        pending.drain_all(|loc| push_bounded(&mut collected, loc));
540
541        // Sort to ensure causal order (parents before children).
542        collected.sort();
543
544        Ok(collected)
545    }
546
547    fn get_next(
548        &mut self,
549        target: &mut [u8],
550        provider: &mut impl StorageProvider,
551    ) -> Result<usize, SyncError> {
552        if self.next_send >= self.to_send.len() {
553            self.state = SyncResponderState::Idle;
554            let message = SyncResponseMessage::SyncEnd {
555                session_id: self.session_id()?,
556                max_index: self.message_index as u64,
557                remaining: false,
558            };
559            let length = Self::write(target, message)?;
560            return Ok(length);
561        }
562
563        let (commands, command_data, next_send) = self.get_commands(provider)?;
564
565        let message = SyncResponseMessage::SyncResponse {
566            session_id: self.session_id()?,
567            response_index: self.message_index as u64,
568            commands,
569        };
570        self.message_index = self
571            .message_index
572            .checked_add(1)
573            .assume("message_index overflow")?;
574        self.next_send = next_send;
575
576        let length = Self::write(target, message)?;
577        let total_length = length
578            .checked_add(command_data.len())
579            .assume("length + command_data_length mustn't overflow")?;
580        target
581            .get_mut(length..total_length)
582            .assume("sync message fits in target")?
583            .copy_from_slice(&command_data);
584        Ok(total_length)
585    }
586
587    /// Writes a sync push message to target for the peer. The message will
588    /// contain any commands that are after the commands in response_cache.
589    pub fn push(
590        &mut self,
591        target: &mut [u8],
592        provider: &mut impl StorageProvider,
593        buffers: &mut TraversalBuffers,
594    ) -> Result<usize, SyncError> {
595        use SyncResponderState as S;
596        let Some(graph_id) = self.graph_id else {
597            self.state = S::Reset;
598            bug!("poll called before graph_id was set");
599        };
600
601        let storage = match provider.get_storage(graph_id) {
602            Ok(s) => s,
603            Err(e) => {
604                self.state = S::Reset;
605                return Err(e.into());
606            }
607        };
608        self.to_send = Self::find_needed_segments(&self.has, storage, buffers)?;
609        let (commands, command_data, next_send) = self.get_commands(provider)?;
610        let mut length = 0;
611        if !commands.is_empty() {
612            let message = SyncType::Push {
613                message: SyncResponseMessage::SyncResponse {
614                    session_id: self.session_id()?,
615                    response_index: self.message_index as u64,
616                    commands,
617                },
618                graph_id: self.graph_id.assume("graph id must exist")?,
619            };
620            self.message_index = self
621                .message_index
622                .checked_add(1)
623                .assume("message_index increment overflow")?;
624            self.next_send = next_send;
625
626            length = Self::write_sync_type(target, message)?;
627            let total_length = length
628                .checked_add(command_data.len())
629                .assume("length + command_data_length mustn't overflow")?;
630            target
631                .get_mut(length..total_length)
632                .assume("sync message fits in target")?
633                .copy_from_slice(&command_data);
634            length = total_length;
635        }
636        Ok(length)
637    }
638
639    fn get_commands(
640        &mut self,
641        provider: &mut impl StorageProvider,
642    ) -> Result<
643        (
644            Vec<CommandMeta, COMMAND_RESPONSE_MAX>,
645            Vec<u8, MAX_SYNC_MESSAGE_SIZE>,
646            usize,
647        ),
648        SyncError,
649    > {
650        let Some(graph_id) = self.graph_id.as_ref() else {
651            self.state = SyncResponderState::Reset;
652            bug!("get_next called before graph_id was set");
653        };
654        let storage = match provider.get_storage(*graph_id) {
655            Ok(s) => s,
656            Err(e) => {
657                self.state = SyncResponderState::Reset;
658                return Err(e.into());
659            }
660        };
661        let mut commands: Vec<CommandMeta, COMMAND_RESPONSE_MAX> = Vec::new();
662        let mut command_data: Vec<u8, MAX_SYNC_MESSAGE_SIZE> = Vec::new();
663        let mut index = self.next_send;
664        for i in self.next_send..self.to_send.len() {
665            if commands.is_full() {
666                break;
667            }
668            index = index.checked_add(1).assume("index + 1 mustn't overflow")?;
669            let Some(&location) = self.to_send.get(i) else {
670                self.state = SyncResponderState::Reset;
671                bug!("send index OOB");
672            };
673
674            let segment = storage
675                .get_segment(location)
676                .inspect_err(|_| self.state = SyncResponderState::Reset)?;
677
678            let found = segment.get_from(location);
679
680            for command in &found {
681                let mut policy_length = 0;
682
683                if let Some(policy) = command.policy() {
684                    policy_length = policy.len();
685                    command_data
686                        .extend_from_slice(policy)
687                        .ok()
688                        .assume("command_data is too large")?;
689                }
690
691                let bytes = command.bytes();
692                command_data
693                    .extend_from_slice(bytes)
694                    .ok()
695                    .assume("command_data is too large")?;
696
697                let max_cut = command.max_cut()?;
698                let meta = CommandMeta {
699                    id: command.id(),
700                    priority: command.priority(),
701                    parent: command.parent(),
702                    policy_length: policy_length as u32,
703                    length: bytes.len() as u32,
704                    max_cut,
705                };
706
707                // FIXME(jdygert): Handle segments with more than COMMAND_RESPONSE_MAX commands.
708                commands
709                    .push(meta)
710                    .ok()
711                    .assume("too many commands in segment")?;
712                if commands.is_full() {
713                    break;
714                }
715            }
716        }
717        Ok((commands, command_data, index))
718    }
719
720    fn session_id(&self) -> Result<u128, SyncError> {
721        Ok(self.session_id.assume("session id is set")?)
722    }
723}