Skip to main content

kk_crypto/
entropy_pool.rs

1// Copyright (c) 2026 John A Keeney, Entrouter. All rights reserved.
2// Licensed under the Apache License, Version 2.0 with Additional Terms.
3// NO COMMERCIAL USE without prior written authorization from Entrouter.
4// Unauthorized commercial use will be prosecuted to the fullest extent of the law.
5// See the LICENSE file in the project root for full license information.
6// NOTICE: Removal of this header is a violation of the license.
7
8//! Pre-generated entropy pool for high-throughput encode paths.
9//!
10//! Every `encode()` call invokes `entropy::gather()`, which includes a
11//! 64-iteration thread-jitter loop (~50-200 μs).  For bulk encryption
12//! this overhead is unnecessary: we can pre-generate snapshots in a
13//! background thread and hand them out instantly.
14//!
15//! # Usage
16//! ```no_run
17//! use kk_crypto::{EntropyPool, encode_pooled, encode_aead_pooled};
18//!
19//! let pool = EntropyPool::new(64).unwrap();
20//! let packet = encode_pooled(b"secret", b"hello", &pool).unwrap();
21//! ```
22
23use std::collections::VecDeque;
24use std::sync::{Arc, Condvar, Mutex};
25use std::thread;
26
27use crate::entropy::{self, EntropySnapshot};
28use crate::error::Result;
29
30/// A pre-warmed pool of [`EntropySnapshot`] values for low-latency draws.
31///
32/// The pool spawns a background thread that continuously generates snapshots
33/// via `entropy::gather()` and refills the pool whenever it falls below a
34/// watermark (50% of capacity).  Draws are nearly instant (mutex lock + pop).
35///
36/// If the pool is temporarily exhausted, `draw()` falls back to a synchronous
37/// `entropy::gather()` call so correctness is never compromised.
38pub struct EntropyPool {
39    inner: Arc<PoolInner>,
40}
41
42struct PoolInner {
43    state: Mutex<PoolState>,
44    /// Signaled when the pool drops below the watermark.
45    need_refill: Condvar,
46    /// Signaled when new snapshots are added.
47    snapshot_added: Condvar,
48    capacity: usize,
49    watermark: usize,
50}
51
52struct PoolState {
53    buf: VecDeque<EntropySnapshot>,
54    shutdown: bool,
55}
56
57impl EntropyPool {
58    /// Create a new pool that holds up to `capacity` pre-generated snapshots.
59    ///
60    /// Blocks until at least 8 snapshots (or `capacity`, whichever is smaller)
61    /// are ready.  The background refill thread starts immediately.
62    ///
63    /// # Panics
64    /// If `capacity` is 0.
65    pub fn new(capacity: usize) -> Result<Self> {
66        assert!(capacity > 0, "EntropyPool capacity must be > 0");
67        let capacity = capacity.clamp(8, 1024);
68        let watermark = capacity / 2;
69        let prewarm = capacity.min(8);
70
71        let inner = Arc::new(PoolInner {
72            state: Mutex::new(PoolState {
73                buf: VecDeque::with_capacity(capacity),
74                shutdown: false,
75            }),
76            need_refill: Condvar::new(),
77            snapshot_added: Condvar::new(),
78            capacity,
79            watermark,
80        });
81
82        // Spawn background refill thread
83        let worker = Arc::clone(&inner);
84        thread::spawn(move || refill_loop(worker));
85
86        // Signal the worker to start filling
87        inner.need_refill.notify_one();
88
89        // Block until pre-warmed
90        {
91            let mut state = inner.state.lock().unwrap();
92            while state.buf.len() < prewarm {
93                state = inner.snapshot_added.wait(state).unwrap();
94            }
95        }
96
97        Ok(Self { inner })
98    }
99
100    /// Draw a single [`EntropySnapshot`] from the pool.
101    ///
102    /// If the pool has snapshots ready, this is a fast mutex-pop (~ns).
103    /// If the pool is exhausted, falls back to synchronous `entropy::gather()`.
104    pub fn draw(&self) -> Result<EntropySnapshot> {
105        let mut state = self.inner.state.lock().unwrap();
106        if let Some(snap) = state.buf.pop_front() {
107            let below_watermark = state.buf.len() < self.inner.watermark;
108            drop(state);
109            if below_watermark {
110                self.inner.need_refill.notify_one();
111            }
112            Ok(snap)
113        } else {
114            drop(state);
115            // Pool exhausted - fall back to synchronous gather
116            entropy::gather()
117        }
118    }
119
120    /// Number of snapshots currently available (approximate).
121    pub fn len(&self) -> usize {
122        self.inner.state.lock().unwrap().buf.len()
123    }
124
125    /// Returns `true` if the pool currently has no snapshots.
126    pub fn is_empty(&self) -> bool {
127        self.len() == 0
128    }
129}
130
131impl Drop for EntropyPool {
132    fn drop(&mut self) {
133        let mut state = self.inner.state.lock().unwrap();
134        state.shutdown = true;
135        drop(state);
136        self.inner.need_refill.notify_one();
137    }
138}
139
140/// Background loop: generate snapshots until the pool is full, then park
141/// until woken by the watermark signal.
142fn refill_loop(inner: Arc<PoolInner>) {
143    loop {
144        let mut state = inner.state.lock().unwrap();
145
146        // Wait until we need to refill or are told to shut down
147        while !state.shutdown && state.buf.len() >= inner.capacity {
148            state = inner.need_refill.wait(state).unwrap();
149        }
150
151        if state.shutdown {
152            return;
153        }
154
155        let slots = inner.capacity - state.buf.len();
156        drop(state);
157
158        // Generate snapshots outside the lock
159        for _ in 0..slots {
160            // Check shutdown between generations
161            {
162                let st = inner.state.lock().unwrap();
163                if st.shutdown {
164                    return;
165                }
166            }
167
168            if let Ok(snap) = entropy::gather() {
169                let mut state = inner.state.lock().unwrap();
170                if state.buf.len() < inner.capacity {
171                    state.buf.push_back(snap);
172                    drop(state);
173                    inner.snapshot_added.notify_one();
174                }
175            }
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn pool_creates_and_prewarms() {
186        let pool = EntropyPool::new(16).unwrap();
187        assert!(pool.len() >= 8, "pool should pre-warm at least 8 snapshots");
188    }
189
190    #[test]
191    fn draw_returns_valid_snapshot() {
192        let pool = EntropyPool::new(16).unwrap();
193        let snap = pool.draw().unwrap();
194        assert_ne!(snap.bytes, [0u8; 32], "snapshot bytes must be non-zero");
195        assert_ne!(snap.timestamp_nanos, 0, "timestamp must be non-zero");
196    }
197
198    #[test]
199    fn successive_draws_differ() {
200        let pool = EntropyPool::new(16).unwrap();
201        let s1 = pool.draw().unwrap();
202        let s2 = pool.draw().unwrap();
203        assert_ne!(
204            s1.bytes, s2.bytes,
205            "two draws must produce different snapshots"
206        );
207    }
208
209    #[test]
210    fn draw_under_exhaustion_still_works() {
211        // Create smallest pool, drain it, then draw again (should fallback)
212        let pool = EntropyPool::new(8).unwrap();
213        // Drain all pre-warmed snapshots
214        for _ in 0..8 {
215            let _ = pool.draw().unwrap();
216        }
217        // This should either get a refilled one or fall back to gather()
218        let snap = pool.draw().unwrap();
219        assert_ne!(snap.bytes, [0u8; 32]);
220    }
221
222    #[test]
223    fn pool_refills_after_drain() {
224        let pool = EntropyPool::new(16).unwrap();
225        // Drain
226        for _ in 0..8 {
227            let _ = pool.draw().unwrap();
228        }
229        // Give the background thread a moment to refill
230        std::thread::sleep(std::time::Duration::from_secs(2));
231        assert!(!pool.is_empty(), "pool should refill after draws");
232    }
233}