Skip to main content

primitives/correlated_randomness/stream/buffered/
mod.rs

1pub mod config;
2pub mod messages;
3
4use std::{
5    cell::Cell,
6    collections::VecDeque,
7    marker::PhantomData,
8    sync::{
9        atomic::{AtomicU64, Ordering},
10        Arc,
11    },
12};
13
14pub use config::{Buffer, BufferConfig, SharedBufferConfig};
15use log::{debug, error, info};
16pub use messages::PrefetchHandle;
17use parking_lot::RwLock;
18use tokio::{
19    sync::{
20        mpsc::{self, UnboundedReceiver, UnboundedSender},
21        oneshot,
22    },
23    task::JoinHandle,
24};
25
26use crate::correlated_randomness::{
27    generator::CorrelationGenerator,
28    stream::{
29        buffered::{config::try_read_config, messages::Command},
30        errors::CorrelatedStreamError,
31        futures::Next,
32        CorrelatedStream,
33        NextVec,
34        ResyncHandle,
35    },
36    CorrelatedBatch,
37};
38
39/// A unit of background work sent from the dispatcher to the generator task.
40enum Work {
41    /// Generate at least `n` elements and return them.
42    Generate(usize),
43    /// Advance the generator's logical position by `n` without materializing the elements.
44    Skip(usize),
45}
46
47/// Buffering over a correlation generator, providing both demand-driven streaming
48/// and proactive prefetching interfaces. The buffer is refilled in the background by a
49/// dispatcher/generator task pair; `capacity` bounds aggregate outstanding demand and
50/// `refill_threshold` drives proactive top-ups.
51pub struct BufferedStream<PB: CorrelatedBatch, E> {
52    command_sender: UnboundedSender<Command<PB, E>>,
53    _unsync_marker: PhantomData<Cell<()>>,
54    config: Arc<RwLock<BufferConfig>>,
55    /// Logical position: cumulative items delivered through `next_n`. Written by the dispatcher
56    /// (the single source of truth) and read synchronously here. See
57    /// [`CorrelatedStream::position`].
58    position: Arc<AtomicU64>,
59    /// Elements currently held in the buffer, ready for immediate delivery. Written by the
60    /// dispatcher whenever the buffer changes and read synchronously here. See
61    /// [`CorrelatedStream::buffered`].
62    buffered: Arc<AtomicU64>,
63    dispatcher_handle: JoinHandle<()>,
64    generator_handle: JoinHandle<()>,
65}
66
67impl<PB: CorrelatedBatch, E> Buffer for BufferedStream<PB, E> {
68    fn config(&self) -> &Arc<RwLock<BufferConfig>> {
69        &self.config
70    }
71}
72
73impl<
74        PB: CorrelatedBatch,
75        E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug + 'static,
76    > BufferedStream<PB, E>
77{
78    /// Creates a new stream.
79    pub fn new<G: CorrelationGenerator<PB> + Send + 'static>(
80        generator: G,
81        net: G::Net,
82        config: BufferConfig,
83    ) -> Self
84    where
85        E: From<G::Error>,
86    {
87        Self::new_with_shared_config(generator, net, Arc::new(RwLock::new(config)))
88    }
89
90    /// Creates a purely on-demand stream: no proactive refill and effectively unbounded admission,
91    /// so every `next_n` triggers generation of exactly the shortfall (any surplus is buffered).
92    /// Suited to streams whose generator is itself the buffer/limiter (e.g. a dealer client).
93    pub fn new_on_demand<G: CorrelationGenerator<PB> + Send + 'static>(
94        generator: G,
95        net: G::Net,
96    ) -> Self
97    where
98        E: From<G::Error>,
99    {
100        Self::new(
101            generator,
102            net,
103            BufferConfig::lazy(BufferConfig::UNBOUNDED, 0),
104        )
105    }
106
107    /// Creates a new stream.
108    pub fn new_with_shared_config<G: CorrelationGenerator<PB> + Send + 'static>(
109        generator: G,
110        net: G::Net,
111        config: Arc<RwLock<BufferConfig>>,
112    ) -> Self
113    where
114        E: From<G::Error>,
115    {
116        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<Command<PB, E>>();
117        let (work_tx, work_rx) = mpsc::unbounded_channel::<Work>();
118        let (items_tx, items_rx) = mpsc::unbounded_channel::<Result<Vec<PB::Item>, E>>();
119        let (skip_tx, skip_rx) = mpsc::unbounded_channel::<Result<(), E>>();
120        let position = Arc::new(AtomicU64::new(0));
121        let buffered = Arc::new(AtomicU64::new(0));
122        let generator_handle =
123            tokio::spawn(generator_loop(generator, net, work_rx, items_tx, skip_tx));
124        let dispatcher_handle = tokio::spawn(dispatcher_loop(
125            cmd_rx,
126            work_tx,
127            items_rx,
128            skip_rx,
129            config.clone(),
130            position.clone(),
131            buffered.clone(),
132            G::SUPPORTS_UNILATERAL_SKIP,
133        ));
134        Self {
135            command_sender: cmd_tx,
136            _unsync_marker: PhantomData,
137            config,
138            position,
139            buffered,
140            dispatcher_handle,
141            generator_handle,
142        }
143    }
144
145    /// Shuts down the buffer gracefully and waits for all background tasks to exit.
146    ///
147    /// Closing the command channel causes the dispatcher to exit, which in turn drops the work
148    /// channel, causing the generator to exit.  Both tasks are awaited before returning.
149    pub async fn stop(self) {
150        let Self {
151            command_sender,
152            dispatcher_handle,
153            generator_handle,
154            ..
155        } = self;
156        drop(command_sender);
157        let _ = dispatcher_handle.await;
158        let _ = generator_handle.await;
159    }
160}
161
162// ============================
163// ===== Generator Task =======
164// ============================
165
166async fn generator_loop<
167    PB: CorrelatedBatch,
168    G: CorrelationGenerator<PB> + Send,
169    E: From<G::Error> + Send,
170>(
171    mut generator: G,
172    mut net: G::Net,
173    mut work_rx: UnboundedReceiver<Work>,
174    items_tx: UnboundedSender<Result<Vec<PB::Item>, E>>,
175    skip_tx: UnboundedSender<Result<(), E>>,
176) {
177    let log_prefix = format!("<Generator<{}>>", std::any::type_name::<PB>());
178    while let Some(work) = work_rx.recv().await {
179        match work {
180            Work::Generate(n) => {
181                debug!("{log_prefix} generating {n} elements");
182                let result = generator.run_for(n, &mut net).await.map_err(E::from);
183                let stop = result.is_err();
184                // Stop on either a generator error or a dropped dispatcher.
185                if items_tx.send(result).is_err() || stop {
186                    break;
187                }
188            }
189            Work::Skip(n) => {
190                debug!("{log_prefix} skipping {n} elements");
191                let result = generator.skip(n, &mut net).await.map_err(E::from);
192                let stop = result.is_err();
193                if skip_tx.send(result).is_err() || stop {
194                    break;
195                }
196            }
197        }
198    }
199    info!("{log_prefix} exiting");
200}
201
202// ============================
203// ===== Dispatcher Task ======
204// ============================
205
206type ItemsCollected<PB> = Vec<<PB as IntoIterator>::Item>;
207type TotalNeeded = usize;
208type BatchSender<PB, E> = oneshot::Sender<Result<Vec<<PB as IntoIterator>::Item>, E>>;
209
210/// Dispatches a pending resync skip to the generator once no other work is in flight.
211fn maybe_skip(
212    pending_skip: &mut usize,
213    work_in_flight: &mut bool,
214    work_tx: &UnboundedSender<Work>,
215    log_prefix: &str,
216) {
217    if *pending_skip > 0 && !*work_in_flight {
218        debug!("{log_prefix} requesting skip of {} elements", *pending_skip);
219        *work_in_flight = true;
220        let _ = work_tx.send(Work::Skip(*pending_skip));
221        *pending_skip = 0;
222    }
223}
224
225/// Issues a generation order if there is unmet demand and nothing else is running (a pending resync
226/// skip takes priority). Demand = `pending_batches` shortfall + `max(refill top-up, prefetch
227/// deficit)`. `Err` means the config lock timed out and the dispatcher should stop.
228#[allow(clippy::too_many_arguments)]
229fn maybe_generate<PB: CorrelatedBatch, E: From<CorrelatedStreamError>>(
230    work_in_flight: &mut bool,
231    pending_resync: &Option<(usize, oneshot::Sender<Result<(), E>>)>,
232    pending_skip: usize,
233    config: &Arc<RwLock<BufferConfig>>,
234    pending_batches: &VecDeque<(ItemsCollected<PB>, TotalNeeded, BatchSender<PB, E>)>,
235    buffer_len: usize,
236    prefetch_demand: usize,
237    work_tx: &UnboundedSender<Work>,
238    log_prefix: &str,
239) -> Result<(), E> {
240    if *work_in_flight || pending_resync.is_some() || pending_skip != 0 {
241        return Ok(());
242    }
243    let batch_shortfall: usize = pending_batches
244        .iter()
245        .map(|(collected, needed, _)| needed.saturating_sub(collected.len() + buffer_len))
246        .sum();
247    let buf_after = buffer_len.saturating_sub(batch_shortfall);
248    let need = batch_shortfall
249        + try_read_config(config)?
250            .refill_threshold()
251            .saturating_sub(buf_after)
252            .max(prefetch_demand);
253    if need > 0 {
254        debug!("{log_prefix} requesting generation of {need} items");
255        *work_in_flight = true;
256        let _ = work_tx.send(Work::Generate(need));
257    }
258    Ok(())
259}
260
261#[allow(clippy::too_many_arguments)]
262async fn dispatcher_loop<
263    PB: CorrelatedBatch,
264    E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug,
265>(
266    mut cmd_rx: UnboundedReceiver<Command<PB, E>>,
267    work_tx: UnboundedSender<Work>,
268    mut items_rx: UnboundedReceiver<Result<Vec<PB::Item>, E>>,
269    mut skip_rx: UnboundedReceiver<Result<(), E>>,
270    config: Arc<RwLock<BufferConfig>>,
271    shared_position: Arc<AtomicU64>,
272    shared_buffered: Arc<AtomicU64>,
273    supports_skip: bool,
274) {
275    let log_prefix = format!("<Dispatcher<{}>>", std::any::type_name::<PB>());
276
277    // Initial capacity is just a hint (the buffer auto-grows on push), clamped so we never
278    // pre-allocate a `BufferConfig::UNBOUNDED` capacity (e.g. from `new_on_demand`).
279    let initial_cap = try_read_config(&config)
280        .map(|c| c.capacity())
281        .unwrap_or(0)
282        .min(1 << 12);
283    let mut buffer: VecDeque<PB::Item> = VecDeque::with_capacity(initial_cap);
284    // Newly generated items still owed to outstanding prefetches.
285    let mut prefetch_demand: usize = 0;
286    // (remaining deficit of newly generated items, completion sender) per outstanding prefetch.
287    let mut prefetch_completions: VecDeque<(usize, oneshot::Sender<Result<(), E>>)> =
288        VecDeque::new();
289    // A single unit of background work (generation or skip) may be outstanding at a time.
290    let mut work_in_flight = false;
291    // Outstanding `next_n` batch requests in FIFO order: (items collected, total needed, sender).
292    let mut pending_batches: VecDeque<(ItemsCollected<PB>, TotalNeeded, BatchSender<PB, E>)> =
293        VecDeque::new();
294    // The logical position lives solely in `shared_position` (the dispatcher is its only writer):
295    // advanced with `fetch_add`, read back with `load` where needed.
296    // An in-flight resync awaiting generator skip completion: (deficit being skipped, completion).
297    // The deficit (not an absolute target) is added to `position` on completion so concurrent
298    // `next_n` increments are preserved.
299    let mut pending_resync: Option<(usize, oneshot::Sender<Result<(), E>>)> = None;
300    // Remaining elements to skip at the generator for the in-flight resync (not yet dispatched).
301    let mut pending_skip: usize = 0;
302    // If a config lock acquisition times out, we propagate this error to all consumers.
303    let mut shutdown_err: Option<E> = None;
304
305    // Lock the config with timeout; on failure, record the error and `break` the outer loop.
306    macro_rules! lock_cfg {
307        () => {
308            match try_read_config(&config) {
309                Ok(g) => g,
310                Err(e) => {
311                    error!("{log_prefix} config lock timeout, shutting down");
312                    shutdown_err = Some(e.into());
313                    break;
314                }
315            }
316        };
317    }
318
319    // Canonical unmet demand: batch shortfalls + prefetch deficits, net of buffered items.
320    macro_rules! outstanding_demand {
321        () => {{
322            let batch_shortfall: usize = pending_batches
323                .iter()
324                .map(|(collected, needed, _)| needed.saturating_sub(collected.len()))
325                .sum();
326            (batch_shortfall + prefetch_demand).saturating_sub(buffer.len())
327        }};
328    }
329
330    // Publishes the current buffer occupancy for synchronous front-end reads.
331    let set_buffered =
332        |buffer_len: usize| shared_buffered.store(buffer_len as u64, Ordering::Release);
333
334    loop {
335        tokio::select! {
336            cmd = cmd_rx.recv() => {
337                let Some(cmd) = cmd else {
338                    info!("{log_prefix} command channel closed, shutting down");
339                    break;
340                };
341                match cmd {
342                    Command::RequestN { n_elements, completion } => {
343                        debug!("{log_prefix} batch request for {n_elements} items");
344                        let cap = lock_cfg!().capacity();
345                        if outstanding_demand!() + n_elements > cap {
346                            // Rejected: nothing is delivered, so the position does not advance.
347                            let _ = completion.send(Err(CorrelatedStreamError::RateLimitExceeded.into()));
348                        } else if buffer.len() >= n_elements {
349                            // Fully served from buffer — complete immediately.
350                            let items: Vec<_> = buffer.drain(..n_elements).collect();
351                            shared_position.fetch_add(n_elements as u64, Ordering::Release);
352                            set_buffered(buffer.len());
353                            let _ = completion.send(Ok(items));
354                        } else {
355                            // Partially served; need generation for the remainder. The request is
356                            // admitted (committed to deliver in FIFO order), so advance now.
357                            let collected: Vec<_> = buffer.drain(..).collect();
358                            shared_position.fetch_add(n_elements as u64, Ordering::Release);
359                            set_buffered(buffer.len());
360                            pending_batches.push_back((collected, n_elements, completion));
361                        }
362                    }
363                    Command::Resync { target, completion } => {
364                        // Snapshot the single source of truth for this arm's checks.
365                        let position = shared_position.load(Ordering::Relaxed);
366                        debug!("{log_prefix} resync to {target} (position {position})");
367                        if pending_resync.is_some() {
368                            // Reject overlapping resyncs: queueing them would only widen the
369                            // window for another desync. The caller must serialize resyncs.
370                            let _ = completion.send(Err(CorrelatedStreamError::ResyncInProgress.into()));
371                        } else if target < position {
372                            let _ = completion.send(Err(CorrelatedStreamError::ResyncRewind {
373                                current: position,
374                                target,
375                            }
376                            .into()));
377                        } else {
378                            // Discard already-buffered elements first (free, local).
379                            let skip = (target - position) as usize;
380                            let drained = skip.min(buffer.len());
381                            buffer.drain(..drained);
382                            shared_position.fetch_add(drained as u64, Ordering::Release);
383                            set_buffered(buffer.len());
384                            let deficit = skip - drained;
385                            if deficit == 0 {
386                                let _ = completion.send(Ok(()));
387                            } else if !supports_skip {
388                                // By the lockstep argument, an interactive generator should always
389                                // have the target buffered; a deficit signals a generation desync.
390                                let _ = completion.send(Err(CorrelatedStreamError::ResyncUnsupported {
391                                    generated: position + drained as u64,
392                                    target,
393                                }
394                                .into()));
395                            } else {
396                                // Skip the remainder at the generator, then finalize on completion.
397                                // We stash the deficit (not `target`) so the completion advances
398                                // position relatively, preserving any concurrent next_n increments.
399                                pending_resync = Some((deficit, completion));
400                                pending_skip = deficit;
401                                maybe_skip(&mut pending_skip, &mut work_in_flight, &work_tx, &log_prefix);
402                            }
403                        }
404                    }
405                    Command::Prefetch { n_elements, completion } => {
406                        debug!("{log_prefix} prefetch {n_elements} items");
407                        if buffer.len() >= n_elements {
408                            // Buffer already covers the demand (includes n_elements == 0).
409                            let _ = completion.send(Ok(()));
410                        } else {
411                            let cap = lock_cfg!().capacity();
412                            if outstanding_demand!() + n_elements > cap {
413                                let _ = completion.send(Err(CorrelatedStreamError::RateLimitExceeded.into()));
414                            } else {
415                                let deficit = n_elements - buffer.len();
416                                prefetch_demand += deficit;
417                                prefetch_completions.push_back((deficit, completion));
418                            }
419                        }
420                    }
421                }
422            }
423
424            result = items_rx.recv() => {
425                let Some(result) = result else {
426                    info!("{log_prefix} generator channel closed, shutting down");
427                    break;
428                };
429                match result {
430                    Ok(items) => {
431                        work_in_flight = false;
432                        let generated = items.len();
433                        debug!("{log_prefix} received {generated} items (pending_batches: {}, buffer: {})",
434                               pending_batches.iter().map(|(c, n, _)| n - c.len()).sum::<usize>(),
435                               buffer.len());
436                        let mut iter = items.into_iter();
437                        // 1. Fill outstanding batch requests in FIFO order.
438                        while let Some((ref mut collected, needed, _)) = pending_batches.front_mut() {
439                            let shortfall = *needed - collected.len();
440                            collected.extend(iter.by_ref().take(shortfall));
441                            if collected.len() < *needed { break; } // still waiting
442                            let (collected, _, tx) = pending_batches.pop_front().unwrap();
443                            let _ = tx.send(Ok(collected));
444                        }
445                        // 2. Buffer the rest; capacity is an admission bound, not a storage bound.
446                        buffer.extend(iter);
447                        set_buffered(buffer.len());
448                        // 3. Credit all newly generated items against prefetch deficits (FIFO).
449                        prefetch_demand = prefetch_demand.saturating_sub(generated);
450                        let mut credit = generated;
451                        while credit > 0 {
452                            let Some((deficit, _)) = prefetch_completions.front_mut() else { break; };
453                            let used = (*deficit).min(credit);
454                            *deficit -= used;
455                            credit -= used;
456                            if *deficit > 0 { break; }
457                            let (_, tx) = prefetch_completions.pop_front().unwrap();
458                            let _ = tx.send(Ok(()));
459                        }
460                        // A resync may have been waiting for this generation to free the work slot.
461                        maybe_skip(&mut pending_skip, &mut work_in_flight, &work_tx, &log_prefix);
462                    }
463                    Err(e) => {
464                        error!("{log_prefix} generation error, shutting down: {e:?}");
465                        for (_, _, tx) in pending_batches.drain(..) { let _ = tx.send(Err(e.clone())); }
466                        for (_, tx) in prefetch_completions.drain(..) { let _ = tx.send(Err(e.clone())); }
467                        if let Some((_, tx)) = pending_resync.take() { let _ = tx.send(Err(e.clone())); }
468                        return;
469                    }
470                }
471            }
472
473            result = skip_rx.recv() => {
474                let Some(result) = result else {
475                    info!("{log_prefix} skip channel closed, shutting down");
476                    break;
477                };
478                work_in_flight = false;
479                match result {
480                    Ok(()) => {
481                        // The generator advanced its position; finalize the in-flight resync.
482                        // Advancing by the deficit (rather than assigning the target) preserves
483                        // any next_n increments that landed while the skip was in flight.
484                        if let Some((deficit, tx)) = pending_resync.take() {
485                            shared_position.fetch_add(deficit as u64, Ordering::Release);
486                            let _ = tx.send(Ok(()));
487                        }
488                    }
489                    Err(e) => {
490                        error!("{log_prefix} skip error, shutting down: {e:?}");
491                        for (_, _, tx) in pending_batches.drain(..) { let _ = tx.send(Err(e.clone())); }
492                        for (_, tx) in prefetch_completions.drain(..) { let _ = tx.send(Err(e.clone())); }
493                        if let Some((_, tx)) = pending_resync.take() { let _ = tx.send(Err(e.clone())); }
494                        return;
495                    }
496                }
497            }
498        }
499
500        // After handling an event, (re)issue generation if there is unmet demand.
501        if let Err(e) = maybe_generate::<PB, E>(
502            &mut work_in_flight,
503            &pending_resync,
504            pending_skip,
505            &config,
506            &pending_batches,
507            buffer.len(),
508            prefetch_demand,
509            &work_tx,
510            &log_prefix,
511        ) {
512            error!("{log_prefix} config lock timeout, shutting down");
513            shutdown_err = Some(e);
514            break;
515        }
516    }
517
518    // Graceful shutdown: resolve all outstanding consumers/handles with the recorded error
519    // (if shutdown was triggered by a lock timeout) or `StreamClosed` otherwise.
520    let final_err: E = shutdown_err.unwrap_or_else(|| CorrelatedStreamError::StreamClosed.into());
521    for (_, _, tx) in pending_batches.drain(..) {
522        let _ = tx.send(Err(final_err.clone()));
523    }
524    for (_, tx) in prefetch_completions.drain(..) {
525        let _ = tx.send(Err(final_err.clone()));
526    }
527    if let Some((_, tx)) = pending_resync.take() {
528        let _ = tx.send(Err(final_err.clone()));
529    }
530}
531
532impl<
533        PB: CorrelatedBatch,
534        E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug + 'static,
535    > CorrelatedStream<PB::Item> for BufferedStream<PB, E>
536{
537    type Error = E;
538
539    fn next_n(&self, n_elements: usize) -> Result<NextVec<PB::Item, E>, CorrelatedStreamError> {
540        if n_elements == 0 {
541            return Ok(NextVec::default());
542        }
543        let max_allowed = self.max_request_size()?;
544        if n_elements > max_allowed {
545            return Err(CorrelatedStreamError::RequestTooLarge {
546                requested: n_elements,
547                max_allowed,
548            });
549        }
550        let (tx, rx) = oneshot::channel();
551        self.command_sender
552            .send(Command::RequestN {
553                n_elements,
554                completion: tx,
555            })
556            .map_err(|e| CorrelatedStreamError::SendError(e.to_string()))?;
557        Ok(NextVec {
558            future: Next(rx),
559            size: n_elements,
560        })
561    }
562
563    fn prefetch_n(&self, n_elements: usize) -> PrefetchHandle<E> {
564        let (tx, rx) = oneshot::channel();
565        let max = match self.max_request_size() {
566            Ok(m) => m,
567            Err(e) => {
568                let _ = tx.send(Err(e.into()));
569                return PrefetchHandle::from(rx);
570            }
571        };
572        if n_elements > max {
573            let _ = tx.send(Err(CorrelatedStreamError::RequestTooLarge {
574                requested: n_elements,
575                max_allowed: max,
576            }
577            .into()));
578            return PrefetchHandle::from(rx);
579        }
580        // If the dispatcher is gone, resolve the handle immediately.
581        let cmd = Command::Prefetch {
582            n_elements,
583            completion: tx,
584        };
585        if let Err(e) = self.command_sender.send(cmd) {
586            if let Command::Prefetch { completion, .. } = e.0 {
587                let _ = completion.send(Err(CorrelatedStreamError::StreamClosed.into()));
588            }
589        }
590        PrefetchHandle::from(rx)
591    }
592
593    fn position(&self) -> u64 {
594        self.position.load(Ordering::Acquire)
595    }
596
597    fn buffered(&self) -> u64 {
598        self.buffered.load(Ordering::Acquire)
599    }
600
601    fn resync(&self, target: u64) -> ResyncHandle<E> {
602        let (tx, rx) = oneshot::channel();
603        let cmd = Command::Resync {
604            target,
605            completion: tx,
606        };
607        if let Err(e) = self.command_sender.send(cmd) {
608            if let Command::Resync { completion, .. } = e.0 {
609                let _ = completion.send(Err(CorrelatedStreamError::StreamClosed.into()));
610            }
611        }
612        ResyncHandle::from(rx)
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use std::{
619        sync::{
620            atomic::{AtomicUsize, Ordering},
621            Arc,
622        },
623        time::Duration,
624    };
625
626    use rand::{rngs::StdRng, SeedableRng};
627    use typenum::U2;
628
629    use crate::{
630        algebra::elliptic_curve::{Curve25519Ristretto, ScalarField},
631        correlated_randomness::{
632            generator::CorrelationGenerator,
633            singlets::{Singlet, Singlets},
634            stream::{
635                buffered::{Buffer, BufferConfig, BufferedStream},
636                errors::CorrelatedStreamError,
637                CorrelatedStream,
638            },
639        },
640        random::Random,
641        utils::TryFuture,
642    };
643
644    type Fq = ScalarField<Curve25519Ristretto>;
645    type TestPB = Singlets<Fq, U2>;
646    type TestItem = Singlet<Fq>;
647    type TestErr = CorrelatedStreamError;
648
649    // -----------------------
650    // ===== Mock generator
651    // -----------------------
652
653    #[derive(Clone)]
654    struct MockGenConfig {
655        /// Sleep for this duration on every `run_for` call to simulate generation latency.
656        delay: Duration,
657        /// If `Some(threshold)`, fail (return `StreamClosed`) once this many items have been
658        /// generated cumulatively.
659        fail_after: Option<usize>,
660    }
661
662    impl Default for MockGenConfig {
663        fn default() -> Self {
664            Self {
665                delay: Duration::from_millis(0),
666                fail_after: None,
667            }
668        }
669    }
670
671    struct MockGen {
672        rng: StdRng,
673        cfg: MockGenConfig,
674        /// Total items produced (visible to tests via `Arc`).
675        items_produced: Arc<AtomicUsize>,
676        /// Number of `run_for` calls made (i.e. number of generation batches).
677        batches: Arc<AtomicUsize>,
678    }
679
680    impl MockGen {
681        fn new(cfg: MockGenConfig) -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
682            let items_produced = Arc::new(AtomicUsize::new(0));
683            let batches = Arc::new(AtomicUsize::new(0));
684            let gen = Self {
685                rng: StdRng::from_seed([0u8; 32]),
686                cfg,
687                items_produced: items_produced.clone(),
688                batches: batches.clone(),
689            };
690            (gen, items_produced, batches)
691        }
692    }
693
694    impl CorrelationGenerator<TestPB> for MockGen {
695        type Net = ();
696        type Error = TestErr;
697
698        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
699            async move { Err(CorrelatedStreamError::StreamClosed) }
700        }
701
702        fn run_for(
703            &mut self,
704            n: usize,
705            _net: &mut (),
706        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
707            async move {
708                self.batches.fetch_add(1, Ordering::SeqCst);
709                if self.cfg.delay > Duration::ZERO {
710                    tokio::time::sleep(self.cfg.delay).await;
711                }
712                if let Some(threshold) = self.cfg.fail_after {
713                    if self.items_produced.load(Ordering::SeqCst) + n > threshold {
714                        return Err(CorrelatedStreamError::StreamClosed);
715                    }
716                }
717                let items: Vec<TestItem> = (0..n)
718                    .map(|_| {
719                        Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, 1)
720                            .into_iter()
721                            .next()
722                            .unwrap()
723                    })
724                    .collect();
725                self.items_produced.fetch_add(n, Ordering::SeqCst);
726                Ok(items)
727            }
728        }
729    }
730
731    fn make_stream(
732        cfg: MockGenConfig,
733        buf_cfg: BufferConfig,
734    ) -> (
735        BufferedStream<TestPB, TestErr>,
736        Arc<AtomicUsize>,
737        Arc<AtomicUsize>,
738    ) {
739        let (gen, produced, batches) = MockGen::new(cfg);
740        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), buf_cfg);
741        (stream, produced, batches)
742    }
743
744    // ----------------------------------------------------------------------
745    // ===== Skip-capable mock generator (dealer-like, deterministic-per-index)
746    // ----------------------------------------------------------------------
747
748    /// A generator that advertises [`CorrelationGenerator::SUPPORTS_UNILATERAL_SKIP`] and
749    /// implements `skip` by advancing its produced counter without emitting items.
750    struct SkipMockGen {
751        rng: StdRng,
752        produced: Arc<AtomicUsize>,
753        skipped: Arc<AtomicUsize>,
754    }
755
756    impl SkipMockGen {
757        fn new() -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
758            let produced = Arc::new(AtomicUsize::new(0));
759            let skipped = Arc::new(AtomicUsize::new(0));
760            let gen = Self {
761                rng: StdRng::from_seed([7u8; 32]),
762                produced: produced.clone(),
763                skipped: skipped.clone(),
764            };
765            (gen, produced, skipped)
766        }
767    }
768
769    impl CorrelationGenerator<TestPB> for SkipMockGen {
770        type Net = ();
771        type Error = TestErr;
772
773        const SUPPORTS_UNILATERAL_SKIP: bool = true;
774
775        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
776            async move { Err(CorrelatedStreamError::StreamClosed) }
777        }
778
779        fn run_for(
780            &mut self,
781            n: usize,
782            _net: &mut (),
783        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
784            async move {
785                let items: Vec<TestItem> = Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, n);
786                self.produced.fetch_add(n, Ordering::SeqCst);
787                Ok(items)
788            }
789        }
790
791        fn skip(
792            &mut self,
793            n: usize,
794            _net: &mut (),
795        ) -> impl TryFuture<Ok = (), Error = Self::Error> {
796            async move {
797                // Advance the logical position by `n` without materializing the elements.
798                self.skipped.fetch_add(n, Ordering::SeqCst);
799                self.produced.fetch_add(n, Ordering::SeqCst);
800                Ok(())
801            }
802        }
803    }
804
805    /// A minimal generator with the default (false) skip capability, for testing the
806    /// `ResyncUnsupported` path when a deficit cannot be covered from the buffer.
807    struct NoSkipMockGen {
808        rng: StdRng,
809    }
810
811    impl CorrelationGenerator<TestPB> for NoSkipMockGen {
812        type Net = ();
813        type Error = TestErr;
814
815        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
816            async move { Err(CorrelatedStreamError::StreamClosed) }
817        }
818
819        fn run_for(
820            &mut self,
821            n: usize,
822            _net: &mut (),
823        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
824            async move { Ok(Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, n)) }
825        }
826    }
827
828    // -----------------------
829    // ===== Tests
830    // -----------------------
831
832    #[tokio::test]
833    async fn next_n_resolves_batch_future() {
834        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
835        let fut = stream.next_n(7).expect("request accepted");
836        let items = fut.await.expect("batch resolves");
837        assert_eq!(items.len(), 7);
838    }
839
840    #[tokio::test]
841    async fn request_too_large_rejected() {
842        let (stream, _, _) = make_stream(
843            MockGenConfig::default(),
844            BufferConfig::eager_with(8, 4), // capacity=8, max_request_size=4
845        );
846        match stream.next_n(5) {
847            Err(CorrelatedStreamError::RequestTooLarge {
848                requested: 5,
849                max_allowed: 4,
850            }) => {}
851            Ok(_) => panic!("must reject n>max"),
852            Err(e) => panic!("unexpected error: {e:?}"),
853        }
854    }
855
856    #[tokio::test]
857    async fn rate_limit_when_exceeding_capacity() {
858        // Small capacity + slow generator → the first batch's shortfall (4) plus the second
859        // request (4) exceeds capacity (4), tripping the admission bound.
860        let cfg = MockGenConfig {
861            delay: Duration::from_millis(200),
862            ..Default::default()
863        };
864        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
865        let _f1 = stream.next_n(4).unwrap();
866        // Give the dispatcher a moment to register the first request before issuing the second.
867        tokio::time::sleep(Duration::from_millis(20)).await;
868        let f2 = stream.next_n(4).unwrap();
869        let results = futures::future::join_all(f2).await;
870        assert!(
871            results
872                .iter()
873                .all(|r| matches!(r, Err(CorrelatedStreamError::RateLimitExceeded))),
874            "expected all four to be rate-limited, got {results:?}"
875        );
876    }
877
878    #[tokio::test]
879    async fn prefetch_completes_and_serves_subsequent_requests_quickly() {
880        let cfg = MockGenConfig {
881            delay: Duration::from_millis(100),
882            ..Default::default()
883        };
884        let (stream, produced, _) = make_stream(cfg, BufferConfig::eager(32));
885        let handle = stream.prefetch_n(10);
886        handle.await.expect("prefetch completes");
887        assert!(produced.load(Ordering::SeqCst) >= 10);
888        // Subsequent request should be served from buffer instantaneously
889        // (or at least without waiting for another slow generation cycle).
890        let start = std::time::Instant::now();
891        let items = stream.next_n(10).unwrap().await.expect("served");
892        assert_eq!(items.len(), 10);
893        assert!(
894            start.elapsed() < Duration::from_millis(80),
895            "request should be served from prefetched buffer (took {:?})",
896            start.elapsed()
897        );
898    }
899
900    #[tokio::test]
901    async fn sequential_prefetches_all_resolve() {
902        // Regression: with refill_threshold=0 the first prefetch fills the buffer; the second
903        // must resolve immediately from the buffer instead of waiting for a generation target
904        // that never triggers.
905        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::lazy(16, 0));
906        for i in 0..2 {
907            tokio::time::timeout(Duration::from_secs(1), stream.prefetch_n(4))
908                .await
909                .unwrap_or_else(|_| panic!("prefetch {i} timed out"))
910                .expect("prefetch completes");
911        }
912        // A larger prefetch only partially covered by the buffer must also resolve.
913        tokio::time::timeout(Duration::from_secs(1), stream.prefetch_n(8))
914            .await
915            .expect("partially covered prefetch timed out")
916            .expect("prefetch completes");
917    }
918
919    #[tokio::test]
920    async fn generator_error_propagates_to_pending_consumers() {
921        let cfg = MockGenConfig {
922            delay: Duration::from_millis(20),
923            fail_after: Some(0), // fail on first batch
924        };
925        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(16));
926        let futs = stream.next_n(4).unwrap();
927        let results = futures::future::join_all(futs).await;
928        assert!(
929            results
930                .iter()
931                .all(|r| matches!(r, Err(CorrelatedStreamError::StreamClosed))),
932            "all consumers should receive the generator error"
933        );
934    }
935
936    #[tokio::test]
937    async fn fifo_order_across_two_batches() {
938        // Two requests issued back-to-back must be served in submission order.
939        let cfg = MockGenConfig {
940            delay: Duration::from_millis(40),
941            ..Default::default()
942        };
943        let (stream, _, batches) = make_stream(cfg, BufferConfig::eager(32));
944        let f1 = stream.next_n(3).unwrap();
945        let f2 = stream.next_n(3).unwrap();
946        let (a, b) = tokio::join!(f1, f2);
947        let a = a.expect("first batch resolves");
948        let b = b.expect("second batch resolves");
949        assert_eq!(a.len(), 3);
950        assert_eq!(b.len(), 3);
951        // At least one batch should have been generated (count is implementation-defined).
952        assert!(batches.load(Ordering::SeqCst) >= 1);
953    }
954
955    #[tokio::test]
956    async fn buffer_config_setters_are_visible() {
957        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
958        assert_eq!(stream.capacity().unwrap(), 16);
959        stream.set_capacity(32).unwrap();
960        assert_eq!(stream.capacity().unwrap(), 32);
961    }
962
963    // ── next_n validation
964    // ──────────────────────────────────────────────────────────────────────
965
966    #[tokio::test]
967    async fn next_n_rejects_too_large() {
968        let (stream, _, _) = make_stream(
969            MockGenConfig::default(),
970            BufferConfig::eager_with(8, 4), // capacity=8, max_request_size=4
971        );
972        match stream.next_n(5) {
973            Err(CorrelatedStreamError::RequestTooLarge {
974                requested: 5,
975                max_allowed: 4,
976            }) => {}
977            Ok(_) => panic!("must reject n > max"),
978            Err(e) => panic!("unexpected error: {e:?}"),
979        }
980    }
981
982    #[tokio::test]
983    async fn next_n_rate_limit_when_exceeding_capacity() {
984        // Slow generator → first batch's shortfall is outstanding; a second batch whose demand
985        // pushes the aggregate shortfall over capacity must be rejected with `RateLimitExceeded`.
986        let cfg = MockGenConfig {
987            delay: Duration::from_millis(200),
988            ..Default::default()
989        };
990        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
991        let _f1 = stream.next_n(4).unwrap();
992        // Let the dispatcher register the first RequestBatch before issuing the second.
993        tokio::time::sleep(Duration::from_millis(20)).await;
994        let f2 = stream.next_n(4).unwrap();
995        assert!(
996            matches!(f2.await, Err(CorrelatedStreamError::RateLimitExceeded)),
997            "second next_n should be rate-limited"
998        );
999    }
1000
1001    #[tokio::test]
1002    async fn next_n_admitted_when_shortfall_fits_capacity() {
1003        // capacity=8, refill_threshold=0: a pending next_n(4) leaves a shortfall of 4, so a
1004        // concurrent next_n(4) (aggregate 8) fits exactly and must be admitted, not rejected.
1005        let cfg = MockGenConfig {
1006            delay: Duration::from_millis(50),
1007            ..Default::default()
1008        };
1009        let (stream, _, _) = make_stream(cfg, BufferConfig::lazy(8, 0));
1010        let f1 = stream.next_n(4).unwrap();
1011        tokio::time::sleep(Duration::from_millis(20)).await;
1012        let f2 = stream.next_n(4).unwrap();
1013        let (a, b) = tokio::join!(f1, f2);
1014        assert_eq!(a.expect("first batch resolves").len(), 4);
1015        assert_eq!(b.expect("second batch resolves").len(), 4);
1016    }
1017
1018    #[tokio::test]
1019    async fn next_n_error_propagates() {
1020        // Generator always fails; the BatchFuture must resolve with the generator error.
1021        let cfg = MockGenConfig {
1022            delay: Duration::from_millis(20), // ensure batch is registered before failure
1023            fail_after: Some(0),
1024        };
1025        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(16));
1026        let result = stream.next_n(4).unwrap().await;
1027        assert!(
1028            matches!(result, Err(CorrelatedStreamError::StreamClosed)),
1029            "expected generator error to propagate through BatchFuture, got {result:?}"
1030        );
1031    }
1032
1033    // ── refill_threshold ───────────────────────────────────────────────────────────────────────
1034
1035    #[tokio::test]
1036    async fn refill_threshold_drives_proactive_generation() {
1037        // lazy buffer: capacity=16, refill_threshold=8 → after a 4-item request, the dispatcher
1038        // should top up to 8 items in the buffer.
1039        let (stream, produced, _) =
1040            make_stream(MockGenConfig::default(), BufferConfig::lazy(16, 8));
1041        let _ = stream.next_n(4).unwrap().await.expect("served");
1042        // Allow the dispatcher to settle (post-serve refill cycle).
1043        tokio::time::sleep(Duration::from_millis(50)).await;
1044        // We requested 4 items; refill_threshold=8 means the buffer should hold at least 8
1045        // additional items beyond what was served, i.e. >= 12 total generated.
1046        let total = produced.load(Ordering::SeqCst);
1047        assert!(total >= 8, "expected >= 8 items generated, got {total}");
1048    }
1049
1050    // ── position counter & resync ────────────────────────────────────────────────────────────
1051
1052    #[tokio::test]
1053    async fn position_tracks_delivered_not_prefetched() {
1054        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(32));
1055        // Prefetching fills the buffer but must NOT advance the logical position.
1056        stream.prefetch_n(10).await.expect("prefetch completes");
1057        assert_eq!(stream.position(), 0, "prefetch must not advance position");
1058        // Delivering through next_n advances it by exactly the requested amount.
1059        let _ = stream.next_n(7).unwrap().await.expect("served");
1060        assert_eq!(stream.position(), 7);
1061        let _ = stream.next_n(3).unwrap().await.expect("served");
1062        assert_eq!(stream.position(), 10);
1063    }
1064
1065    #[tokio::test]
1066    async fn buffered_tracks_ready_elements() {
1067        // Lazy buffer with no proactive refill, so occupancy only changes on prefetch/next_n.
1068        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::lazy(32, 0));
1069        assert_eq!(stream.buffered(), 0);
1070        stream.prefetch_n(10).await.expect("prefetch completes");
1071        assert_eq!(stream.buffered(), 10, "prefetch fills the buffer");
1072        let _ = stream.next_n(4).unwrap().await.expect("served");
1073        assert_eq!(stream.buffered(), 6, "delivery drains the buffer");
1074    }
1075
1076    #[tokio::test]
1077    async fn position_does_not_advance_on_rejected_request() {
1078        // Slow generator + tiny capacity → the second request is rate-limited and delivers
1079        // nothing, so the position must stay at the first request's amount.
1080        let cfg = MockGenConfig {
1081            delay: Duration::from_millis(200),
1082            ..Default::default()
1083        };
1084        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
1085        let _f1 = stream.next_n(4).unwrap();
1086        tokio::time::sleep(Duration::from_millis(20)).await;
1087        let f2 = stream.next_n(4).unwrap();
1088        assert!(matches!(
1089            f2.await,
1090            Err(CorrelatedStreamError::RateLimitExceeded)
1091        ));
1092        // Only the admitted first request counts.
1093        assert_eq!(stream.position(), 4);
1094    }
1095
1096    #[tokio::test]
1097    async fn resync_drains_buffer_and_advances_position() {
1098        // Lazy buffer with no proactive refill, so the only generation is the explicit prefetch;
1099        // this isolates resync's drain from background top-ups.
1100        let (stream, produced, _) =
1101            make_stream(MockGenConfig::default(), BufferConfig::lazy(32, 0));
1102        stream.prefetch_n(10).await.expect("prefetch completes");
1103        let before = produced.load(Ordering::SeqCst);
1104        // Target is fully covered by the buffer: pure local drain, no extra generation.
1105        stream.resync(6).await.expect("resync completes");
1106        assert_eq!(stream.position(), 6);
1107        assert_eq!(
1108            produced.load(Ordering::SeqCst),
1109            before,
1110            "drain-only resync must not generate"
1111        );
1112        // Subsequent delivery continues from the resynced position.
1113        let _ = stream.next_n(2).unwrap().await.expect("served");
1114        assert_eq!(stream.position(), 8);
1115    }
1116
1117    #[tokio::test]
1118    async fn resync_noop_when_already_at_target() {
1119        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
1120        let _ = stream.next_n(5).unwrap().await.expect("served");
1121        stream.resync(5).await.expect("no-op resync completes");
1122        assert_eq!(stream.position(), 5);
1123    }
1124
1125    #[tokio::test]
1126    async fn resync_rewind_is_rejected() {
1127        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
1128        let _ = stream.next_n(5).unwrap().await.expect("served");
1129        match stream.resync(3).await {
1130            Err(CorrelatedStreamError::ResyncRewind {
1131                current: 5,
1132                target: 3,
1133            }) => {}
1134            other => panic!("expected ResyncRewind, got {other:?}"),
1135        }
1136        // Position is unchanged after a rejected rewind.
1137        assert_eq!(stream.position(), 5);
1138    }
1139
1140    #[tokio::test]
1141    async fn resync_unsupported_when_deficit_and_no_skip() {
1142        // Lazy buffer with no proactive refill so nothing is buffered; a resync past the buffer
1143        // needs generator skipping, which this generator does not support.
1144        let gen = NoSkipMockGen {
1145            rng: StdRng::from_seed([0u8; 32]),
1146        };
1147        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(16, 0));
1148        match stream.resync(10).await {
1149            Err(CorrelatedStreamError::ResyncUnsupported {
1150                generated: 0,
1151                target: 10,
1152            }) => {}
1153            other => panic!("expected ResyncUnsupported, got {other:?}"),
1154        }
1155        // Nothing was delivered/skipped, so the position stays put.
1156        assert_eq!(stream.position(), 0);
1157    }
1158
1159    #[tokio::test]
1160    async fn resync_skips_at_generator_when_supported() {
1161        let (gen, produced, skipped) = SkipMockGen::new();
1162        // Lazy buffer, no proactive refill: the resync deficit must be skipped at the generator.
1163        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(64, 0));
1164        stream.resync(10).await.expect("resync via skip completes");
1165        assert_eq!(stream.position(), 10);
1166        assert_eq!(
1167            skipped.load(Ordering::SeqCst),
1168            10,
1169            "deficit should be skipped"
1170        );
1171        assert_eq!(
1172            produced.load(Ordering::SeqCst),
1173            10,
1174            "skip advances the generator without delivering items"
1175        );
1176        // Delivery resumes correctly from the skipped-to position.
1177        let items = stream.next_n(3).unwrap().await.expect("served");
1178        assert_eq!(items.len(), 3);
1179        assert_eq!(stream.position(), 13);
1180    }
1181
1182    #[tokio::test]
1183    async fn resync_partial_buffer_then_skip_remainder() {
1184        let (gen, produced, skipped) = SkipMockGen::new();
1185        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(64, 0));
1186        // Buffer 4 elements, then resync past them: 4 drained locally, 6 skipped at generator.
1187        stream.prefetch_n(4).await.expect("prefetch completes");
1188        let produced_after_prefetch = produced.load(Ordering::SeqCst);
1189        assert_eq!(produced_after_prefetch, 4);
1190        stream.resync(10).await.expect("resync completes");
1191        assert_eq!(stream.position(), 10);
1192        assert_eq!(
1193            skipped.load(Ordering::SeqCst),
1194            6,
1195            "only the unbuffered remainder is skipped at the generator"
1196        );
1197    }
1198}