1use super::*;
14use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{mpsc, Arc, Mutex};
17
18const MAX_QUEUED: usize = 4096;
20
21const MAX_BATCH: usize = 8;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum ProofVerifyThreads {
31 #[default]
38 Auto,
39 Fixed(usize),
41}
42
43impl ProofVerifyThreads {
44 pub(super) fn count(self) -> usize {
46 if !cfg!(feature = "proof-pool") {
47 if matches!(self, ProofVerifyThreads::Fixed(n) if n > 0) {
48 log::warn!("link proofs verified inline: built without the proof-pool feature");
49 }
50 return 0;
51 }
52 match self {
53 ProofVerifyThreads::Fixed(n) => n,
54 ProofVerifyThreads::Auto => {
55 std::thread::available_parallelism().map_or(1, |n| (n.get() / 2).clamp(1, 4))
56 }
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub(crate) enum Owner {
64 Link(LinkId),
66 Packet {
71 destination: [u8; 16],
72 rtt: f64,
73 interface: Option<rns_core::transport::types::InterfaceId>,
74 },
75}
76
77enum Job {
78 Verify {
80 owner: Owner,
81 packet_hash: [u8; 32],
82 signature: [u8; 64],
83 key: Ed25519PublicKey,
84 },
85 Sign {
87 owner: Owner,
88 packet_hash: [u8; 32],
89 key: Ed25519PrivateKey,
90 },
91 Encrypt(Box<Encryption>),
93 Decrypt(Box<Decryption>),
95}
96
97pub(crate) struct Decryption {
99 pub(crate) destination: [u8; 16],
100 pub(crate) packet_hash: [u8; 32],
101 pub(crate) interface: rns_core::transport::types::InterfaceId,
103 pub(crate) ciphertext: Vec<u8>,
104 pub(crate) key: DecryptionKey,
105}
106
107pub(crate) enum DecryptionKey {
109 Ratchets(std::sync::Arc<crate::LocalRatchets>),
111 Identity(std::sync::Arc<rns_crypto::identity::Identity>),
113}
114
115impl Decryption {
116 pub(crate) fn run(&self) -> Option<Vec<u8>> {
118 match &self.key {
119 DecryptionKey::Ratchets(owner) => owner
120 .decrypt(&self.ciphertext)
121 .ok()
122 .map(|decrypted| decrypted.plaintext),
123 DecryptionKey::Identity(identity) => identity.decrypt(&self.ciphertext).ok(),
124 }
125 }
126}
127
128pub(crate) use crate::common::event::DeferredSend as Encryption;
130
131pub(crate) enum Outcome {
132 Verified {
133 owner: Owner,
134 packet_hash: [u8; 32],
135 valid: bool,
136 },
137 Signed {
138 owner: Owner,
139 packet_hash: [u8; 32],
140 signature: [u8; 64],
141 },
142 Encrypted {
144 ticket: u64,
145 destination: [u8; 16],
146 payload: Option<Vec<u8>>,
147 },
148 Decrypted {
150 destination: [u8; 16],
151 packet_hash: [u8; 32],
152 interface: rns_core::transport::types::InterfaceId,
153 plaintext: Option<Vec<u8>>,
154 },
155}
156
157impl Job {
158 fn run(self) -> Outcome {
159 match self {
160 Job::Verify {
161 owner,
162 packet_hash,
163 signature,
164 key,
165 } => Outcome::Verified {
166 owner,
167 packet_hash,
168 valid: key.verify(&signature, &packet_hash),
169 },
170 Job::Sign {
171 owner,
172 packet_hash,
173 key,
174 } => Outcome::Signed {
175 owner,
176 packet_hash,
177 signature: key.sign(&packet_hash),
178 },
179 Job::Encrypt(encryption) => Outcome::Encrypted {
180 ticket: encryption.ticket,
181 destination: encryption.destination,
182 payload: encryption.run(),
183 },
184 Job::Decrypt(decryption) => Outcome::Decrypted {
185 destination: decryption.destination,
186 packet_hash: decryption.packet_hash,
187 interface: decryption.interface,
188 plaintext: decryption.run(),
189 },
190 }
191 }
192}
193
194pub(crate) fn one_cpu() -> bool {
197 static ONE_CPU: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
198 *ONE_CPU
199 .get_or_init(|| std::thread::available_parallelism().map_or(true, |cpus| cpus.get() == 1))
200}
201
202pub(super) struct ProofPool {
203 jobs: Option<mpsc::SyncSender<Job>>,
204 outcomes: mpsc::Receiver<Outcome>,
205 woken: Arc<AtomicBool>,
208 workers: Vec<std::thread::JoinHandle<()>>,
209}
210
211impl ProofPool {
212 pub(super) fn start(threads: usize, wake: crate::event::EventSender) -> std::io::Result<Self> {
213 let (jobs, queue) = mpsc::sync_channel::<Job>(MAX_QUEUED);
214 let queue = Arc::new(Mutex::new(queue));
215 let (outcome_tx, outcomes) = mpsc::channel();
216 let woken = Arc::new(AtomicBool::new(false));
217 let mut workers = Vec::with_capacity(threads);
218 for index in 0..threads {
219 let queue = Arc::clone(&queue);
220 let outcome_tx = outcome_tx.clone();
221 let woken = Arc::clone(&woken);
222 let wake = wake.clone();
223 let worker = std::thread::Builder::new()
224 .name(format!("rns-proof-{index}"))
225 .spawn(move || loop {
226 let mut jobs = Vec::with_capacity(MAX_BATCH);
228 {
229 let queue = queue.lock().unwrap_or_else(|p| p.into_inner());
230 let Ok(job) = queue.recv() else {
231 return;
232 };
233 let checking = matches!(job, Job::Verify { .. });
234 jobs.push(job);
235 while checking && jobs.len() < MAX_BATCH {
236 match queue.try_recv() {
237 Ok(job) => {
238 let check = matches!(job, Job::Verify { .. });
239 jobs.push(job);
240 if !check {
241 break;
242 }
243 }
244 Err(_) => break,
245 }
246 }
247 }
248 for outcome in run_jobs(jobs) {
249 if outcome_tx.send(outcome).is_err() {
250 return;
251 }
252 }
253 if !woken.swap(true, Ordering::AcqRel) {
256 let _ = wake.try_send(crate::event::Event::LinkProofsVerified);
257 }
258 });
259 match worker {
260 Ok(handle) => workers.push(handle),
261 Err(error) if workers.is_empty() => return Err(error),
262 Err(_) => break,
264 }
265 }
266 Ok(Self {
267 jobs: Some(jobs),
268 outcomes,
269 woken,
270 workers,
271 })
272 }
273
274 fn submit(&self, job: Job) -> bool {
275 self.jobs
276 .as_ref()
277 .is_some_and(|jobs| jobs.try_send(job).is_ok())
278 }
279
280 pub(super) fn submit_encrypt(
283 &self,
284 encryption: Box<Encryption>,
285 ) -> Result<(), Box<Encryption>> {
286 let Some(jobs) = &self.jobs else {
287 return Err(encryption);
288 };
289 jobs.try_send(Job::Encrypt(encryption))
290 .map_err(|error| match error {
291 mpsc::TrySendError::Full(Job::Encrypt(encryption))
292 | mpsc::TrySendError::Disconnected(Job::Encrypt(encryption)) => encryption,
293 _ => unreachable!("handed back the job it was given"),
294 })
295 }
296
297 pub(super) fn submit_decrypt(
300 &self,
301 decryption: Box<Decryption>,
302 ) -> Result<(), Box<Decryption>> {
303 let Some(jobs) = &self.jobs else {
304 return Err(decryption);
305 };
306 jobs.try_send(Job::Decrypt(decryption))
307 .map_err(|error| match error {
308 mpsc::TrySendError::Full(Job::Decrypt(decryption))
309 | mpsc::TrySendError::Disconnected(Job::Decrypt(decryption)) => decryption,
310 _ => unreachable!("handed back the job it was given"),
311 })
312 }
313
314 pub(super) fn submit_verify(
317 &self,
318 owner: Owner,
319 packet_hash: [u8; 32],
320 signature: [u8; 64],
321 key: Ed25519PublicKey,
322 ) -> bool {
323 self.submit(Job::Verify {
324 owner,
325 packet_hash,
326 signature,
327 key,
328 })
329 }
330
331 pub(super) fn submit_sign(
334 &self,
335 owner: Owner,
336 packet_hash: [u8; 32],
337 key: Ed25519PrivateKey,
338 ) -> bool {
339 self.submit(Job::Sign {
340 owner,
341 packet_hash,
342 key,
343 })
344 }
345
346 pub(super) fn drain(&self, out: &mut Vec<Outcome>) {
348 self.woken.store(false, Ordering::Release);
350 out.extend(self.outcomes.try_iter());
351 }
352}
353
354impl Drop for ProofPool {
355 fn drop(&mut self) {
356 self.jobs = None;
358 for worker in self.workers.drain(..) {
359 let _ = worker.join();
360 }
361 }
362}
363
364fn run_jobs(jobs: Vec<Job>) -> Vec<Outcome> {
368 let checks = jobs
369 .iter()
370 .filter(|job| matches!(job, Job::Verify { .. }))
371 .count();
372 #[cfg(feature = "proof-pool")]
375 let batch_valid = checks > 1 && {
376 let items: Vec<(&Ed25519PublicKey, &[u8; 64], &[u8])> = jobs
377 .iter()
378 .filter_map(|job| match job {
379 Job::Verify {
380 packet_hash,
381 signature,
382 key,
383 ..
384 } => Some((key, signature, &packet_hash[..])),
385 Job::Sign { .. } | Job::Encrypt(_) | Job::Decrypt(_) => None,
386 })
387 .collect();
388 rns_crypto::ed25519::verify_batch(&items)
389 };
390 #[cfg(not(feature = "proof-pool"))]
391 let batch_valid = {
392 let _ = checks;
393 false
394 };
395 jobs.into_iter()
396 .map(|job| match job {
397 Job::Verify {
398 owner, packet_hash, ..
399 } if batch_valid => Outcome::Verified {
400 owner,
401 packet_hash,
402 valid: true,
403 },
404 job => job.run(),
405 })
406 .collect()
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn check(index: u8, valid: bool) -> Job {
414 let key = Ed25519PrivateKey::from_bytes(&[index + 1; 32]);
415 let packet_hash = [index; 32];
416 let mut signature = key.sign(&packet_hash);
417 if !valid {
418 signature[0] ^= 1;
419 }
420 Job::Verify {
421 owner: Owner::Link([index; 16]),
422 packet_hash,
423 signature,
424 key: key.public_key(),
425 }
426 }
427
428 fn verdicts(jobs: Vec<Job>) -> Vec<bool> {
429 run_jobs(jobs)
430 .into_iter()
431 .map(|outcome| match outcome {
432 Outcome::Verified { valid, .. } => valid,
433 Outcome::Signed { .. } | Outcome::Encrypted { .. } | Outcome::Decrypted { .. } => {
434 true
435 }
436 })
437 .collect()
438 }
439
440 #[test]
441 fn verdicts_name_each_bad_proof_in_a_batch() {
442 let jobs: Vec<Job> = (0..6).map(|i| check(i, i != 3)).collect();
443 assert_eq!(verdicts(jobs), [true, true, true, false, true, true]);
444 let jobs: Vec<Job> = (0..6).map(|i| check(i, true)).collect();
445 assert!(verdicts(jobs).into_iter().all(|valid| valid));
446 assert_eq!(verdicts(vec![check(9, false)]), [false]);
447 }
448
449 #[test]
450 fn a_batch_keeps_order_and_signs_its_proofs() {
451 let key = Ed25519PrivateKey::from_bytes(&[42; 32]);
452 let jobs = vec![
453 check(1, true),
454 Job::Sign {
455 owner: Owner::Link([2; 16]),
456 packet_hash: [2; 32],
457 key: Ed25519PrivateKey::from_bytes(&[42; 32]),
458 },
459 check(3, false),
460 ];
461 let outcomes = run_jobs(jobs);
462 assert!(matches!(outcomes[0], Outcome::Verified { valid: true, .. }));
463 match &outcomes[1] {
464 Outcome::Signed {
465 packet_hash,
466 signature,
467 ..
468 } => assert!(key.public_key().verify(signature, packet_hash)),
469 _ => panic!("expected a signature"),
470 }
471 assert!(matches!(
472 outcomes[2],
473 Outcome::Verified { valid: false, .. }
474 ));
475 }
476}