Skip to main content

rns_net/common/link_manager/
proof_pool.rs

1//! Signing and verification of link packet proofs on worker threads.
2//!
3//! Signing a received link packet's proof, and checking a proof of a packet
4//! sent, are most of what a link message costs once it is out. With threads
5//! to spare, the driver hands each to a small pool and goes on with the
6//! next event; the outcomes come back through a mailbox the driver drains on
7//! every turn. Several workers sign and check proofs at the same time: with
8//! many link packets in flight, signing them one after another on the
9//! driver held every packet behind the ones received with it. Without
10//! threads (one CPU, or none configured) proofs are signed and checked
11//! inline as before.
12
13use super::*;
14use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{mpsc, Arc, Mutex};
17
18/// Proofs queued for the pool at most; past that they are checked inline.
19const MAX_QUEUED: usize = 4096;
20
21/// Proof checks a worker takes together at most: those queued right behind
22/// the one it waited for are checked as one Ed25519 batch, about half the
23/// cost each from 8 on. Kept small so one worker does not take the work of
24/// the others.
25const MAX_BATCH: usize = 8;
26
27/// How many threads verify and sign link proofs. Threads start only in builds with the
28/// `proof-pool` feature; other builds verify every proof inline.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum ProofVerifyThreads {
31    /// Half the CPUs this process may run on, at least one and at most four.
32    /// One thread pays off even on a single CPU: proofs queued behind each
33    /// other are verified as one batch, which inline checking cannot do, and
34    /// the driver keeps reading while proofs are signed. Signing scales
35    /// with threads up to about four; past that, contention on the shared
36    /// queue costs more than the extra threads add.
37    #[default]
38    Auto,
39    /// Exactly this many; 0 checks every proof on the driver thread.
40    Fixed(usize),
41}
42
43impl ProofVerifyThreads {
44    /// Threads to start: none without the `proof-pool` feature.
45    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/// What a proof belongs to.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub(crate) enum Owner {
64    /// A packet sent or received on this link.
65    Link(LinkId),
66    /// A single packet sent to or received for `destination`. For a check,
67    /// `rtt` is the round trip measured when the proof arrived; for a
68    /// signature, `interface` is where the proved packet arrived, which its
69    /// proof goes back on.
70    Packet {
71        destination: [u8; 16],
72        rtt: f64,
73        interface: Option<rns_core::transport::types::InterfaceId>,
74    },
75}
76
77enum Job {
78    /// Check a peer's proof of a packet `owner` sent.
79    Verify {
80        owner: Owner,
81        packet_hash: [u8; 32],
82        signature: [u8; 64],
83        key: Ed25519PublicKey,
84    },
85    /// Sign the proof of a packet `owner` received.
86    Sign {
87        owner: Owner,
88        packet_hash: [u8; 32],
89        key: Ed25519PrivateKey,
90    },
91    /// Encrypt a deferred single packet's payload for `destination`.
92    Encrypt(Box<Encryption>),
93    /// Decrypt a single packet delivered to a local destination.
94    Decrypt(Box<Decryption>),
95}
96
97/// A single packet for a local destination that delivers decrypted.
98pub(crate) struct Decryption {
99    pub(crate) destination: [u8; 16],
100    pub(crate) packet_hash: [u8; 32],
101    /// Where the packet arrived, which its proof goes back on.
102    pub(crate) interface: rns_core::transport::types::InterfaceId,
103    pub(crate) ciphertext: Vec<u8>,
104    pub(crate) key: DecryptionKey,
105}
106
107/// What a delivered packet is decrypted with.
108pub(crate) enum DecryptionKey {
109    /// The destination's ratchets, then its identity, as its owner decrypts.
110    Ratchets(std::sync::Arc<crate::LocalRatchets>),
111    /// The destination's identity.
112    Identity(std::sync::Arc<rns_crypto::identity::Identity>),
113}
114
115impl Decryption {
116    /// The plaintext, or `None` when the packet does not decrypt.
117    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
128/// A deferred send's plaintext and what encrypting it takes.
129pub(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    /// A deferred send's encrypted payload, `None` when it failed.
143    Encrypted {
144        ticket: u64,
145        destination: [u8; 16],
146        payload: Option<Vec<u8>>,
147    },
148    /// A delivered packet's plaintext, `None` when it did not decrypt.
149    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
194/// Whether this process may run on one CPU only, checked once: work handed
195/// to another thread would then only share that CPU.
196pub(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    /// Set by a worker that sent the driver a wake event the driver has not
206    /// yet answered by draining the mailbox.
207    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                    // One job, and the proof checks queued right behind it.
227                    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                    // One wake stands for every outcome until the driver
254                    // drains the mailbox.
255                    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                // Fewer threads than asked for still work.
263                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    /// Queue a deferred send's encryption, or hand it back when the pool is
281    /// full or gone, for the caller to encrypt it inline.
282    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    /// Queue a delivered packet's decryption, or hand it back when the pool
298    /// is full or gone, for the caller to decrypt it inline.
299    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    /// Queue a proof's check. Returns false when the pool is full or gone,
315    /// for the caller to check it inline.
316    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    /// Queue a proof's signature. Returns false when the pool is full or
332    /// gone, for the caller to sign inline.
333    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    /// The outcomes ready so far.
347    pub(super) fn drain(&self, out: &mut Vec<Outcome>) {
348        // Cleared first: an outcome sent after the drain wakes the driver again.
349        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        // Closing the queue ends every worker once it is empty.
357        self.jobs = None;
358        for worker in self.workers.drain(..) {
359            let _ = worker.join();
360        }
361    }
362}
363
364/// Run `jobs` in order, checking their proofs as one batch when there are
365/// several. A batch that fails is checked again proof by proof, to tell
366/// which proofs failed.
367fn run_jobs(jobs: Vec<Job>) -> Vec<Outcome> {
368    let checks = jobs
369        .iter()
370        .filter(|job| matches!(job, Job::Verify { .. }))
371        .count();
372    // Batches need rns-crypto's `ed25519-batch`, which `proof-pool` (the
373    // only build that starts workers) turns on.
374    #[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}