diff --git a/tests/src/lib.rs b/tests/src/lib.rs index c416c76..7e8def6 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -114,4 +114,51 @@ mod tests { assert!(valid); Ok(()) } + + /// Regression guard for the rapidsnark concurrent-proving deadlock. + /// + /// rapidsnark before v0.0.8 shared a single global C++ thread pool with no + /// pool-level lock, so concurrent provers could strand each other on a + /// worker and hang. The hang was non-deterministic (more likely the more + /// proofs ran at once); v0.0.8 serializes the pool and fixes it. + /// + /// This runs several proofs at once with a timeout watchdog, so a + /// reintroduced deadlock fails the test instead of hanging CI forever. It is + /// a best-effort, timing-based check: a failure here almost certainly means + /// the deadlock is back (e.g. an ffiasm downgrade), not random flakiness. + #[test] + fn concurrent_proving_does_not_deadlock() -> Result<()> { + use std::{sync::mpsc, time::Duration}; + + const CONCURRENCY: usize = 8; + const TIMEOUT: Duration = Duration::from_secs(60); + + let zkey_path = "./test-vectors/multiplier2_final.zkey"; + + let mut inputs = HashMap::new(); + inputs.insert("a".to_string(), vec!["3".to_string()]); + inputs.insert("b".to_string(), vec!["11".to_string()]); + let wtns_buffer = compute_witness(inputs, multiplier2_witness)?; + + let (tx, rx) = mpsc::channel(); + for _ in 0..CONCURRENCY { + let wtns = wtns_buffer.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + rust_rapidsnark::groth16_prover_zkey_file_wrapper(zkey_path, wtns).unwrap(); + let _ = tx.send(()); + }); + } + drop(tx); + + for completed in 0..CONCURRENCY { + rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| { + panic!( + "concurrent proving deadlocked: only {completed}/{CONCURRENCY} \ + proofs finished within {TIMEOUT:?}" + ) + }); + } + Ok(()) + } }