Skip to main content

openentropy_core/sources/frontier/
cas_contention.rs

1//! Atomic CAS contention — entropy from multi-thread compare-and-swap arbitration.
2//!
3//! Multiple threads race on atomic CAS operations targeting shared cache lines.
4//! The hardware coherence engine's arbitration order is physically nondeterministic.
5//!
6//! PoC measured H∞ = 2.463 bits/byte (4-thread XOR combined).
7
8use crate::source::{EntropySource, SourceCategory, SourceInfo};
9use crate::sources::helpers::{mach_time, xor_fold_u64};
10
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::thread;
14
15const NUM_THREADS: usize = 4;
16const NUM_TARGETS: usize = 64;
17// 128-byte spacing to put each target on its own Apple Silicon cache line.
18const TARGET_SPACING: usize = 16; // 16 * 8 bytes = 128 bytes
19
20/// Configuration for CAS contention entropy collection.
21#[derive(Debug, Clone)]
22pub struct CASContentionConfig {
23    /// Number of threads to spawn for contention.
24    ///
25    /// More threads increase contention and entropy quality at the cost of CPU.
26    ///
27    /// **Default:** `4`
28    pub num_threads: usize,
29}
30
31impl Default for CASContentionConfig {
32    fn default() -> Self {
33        Self {
34            num_threads: NUM_THREADS,
35        }
36    }
37}
38
39/// CAS contention entropy source.
40///
41/// Spawns multiple threads that perform atomic compare-and-swap on shared
42/// targets spread across cache lines. The arbitration timing between threads
43/// competing for the same cache line is physically nondeterministic because:
44///
45/// 1. The cache coherence protocol (MOESI on Apple Silicon) arbitrates
46///    concurrent exclusive-access requests nondeterministically.
47/// 2. The interconnect fabric latency varies with thermal state and traffic.
48/// 3. Each thread's CAS targets are chosen pseudo-randomly, creating
49///    unpredictable contention patterns.
50/// 4. XOR-combining timings from all threads amplifies the arbitration entropy.
51pub struct CASContentionSource {
52    config: CASContentionConfig,
53}
54
55impl CASContentionSource {
56    pub fn new(config: CASContentionConfig) -> Self {
57        Self { config }
58    }
59}
60
61impl Default for CASContentionSource {
62    fn default() -> Self {
63        Self::new(CASContentionConfig::default())
64    }
65}
66
67static CAS_CONTENTION_INFO: SourceInfo = SourceInfo {
68    name: "cas_contention",
69    description: "Multi-thread atomic CAS arbitration contention jitter",
70    physics: "Spawns 4 threads performing atomic compare-and-swap operations on \
71              shared targets spread across 128-byte-aligned cache lines. The \
72              hardware coherence engine (MOESI protocol on Apple Silicon) must \
73              arbitrate concurrent exclusive-access requests. This arbitration is \
74              physically nondeterministic due to interconnect fabric latency \
75              variations, thermal state, and traffic from other cores/devices. \
76              XOR-combining timing measurements from all threads amplifies the \
77              arbitration entropy. PoC measured H\u{221e} = 2.463 bits/byte.",
78    category: SourceCategory::Frontier,
79    platform_requirements: &[],
80    entropy_rate_estimate: 2000.0,
81    composite: false,
82};
83
84struct ThreadResult {
85    timings: Vec<u64>,
86}
87
88impl EntropySource for CASContentionSource {
89    fn info(&self) -> &SourceInfo {
90        &CAS_CONTENTION_INFO
91    }
92
93    fn is_available(&self) -> bool {
94        true
95    }
96
97    fn collect(&self, n_samples: usize) -> Vec<u8> {
98        let samples_per_thread = n_samples * 4 + 64;
99        let nthreads = self.config.num_threads;
100
101        // Allocate contention targets (each on its own cache line).
102        let total_atomics = NUM_TARGETS * TARGET_SPACING;
103        let targets: Arc<Vec<AtomicU64>> =
104            Arc::new((0..total_atomics).map(|_| AtomicU64::new(0)).collect());
105
106        let go = Arc::new(AtomicU64::new(0));
107        let stop = Arc::new(AtomicU64::new(0));
108
109        let mut handles = Vec::with_capacity(nthreads);
110
111        for thread_id in 0..nthreads {
112            let targets = targets.clone();
113            let go = go.clone();
114            let stop = stop.clone();
115            let count = samples_per_thread;
116
117            handles.push(thread::spawn(move || {
118                let mut timings = Vec::with_capacity(count);
119                let mut lcg: u64 = mach_time() ^ ((thread_id as u64) << 32) | 1;
120
121                // Wait for go signal.
122                while go.load(Ordering::Acquire) == 0 {
123                    std::hint::spin_loop();
124                }
125
126                for _ in 0..count {
127                    if stop.load(Ordering::Relaxed) != 0 {
128                        break;
129                    }
130
131                    lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
132                    let idx = ((lcg >> 32) as usize % NUM_TARGETS) * TARGET_SPACING;
133
134                    let t0 = mach_time();
135
136                    let expected = targets[idx].load(Ordering::Relaxed);
137                    let _ = targets[idx].compare_exchange_weak(
138                        expected,
139                        expected.wrapping_add(1),
140                        Ordering::AcqRel,
141                        Ordering::Relaxed,
142                    );
143
144                    let t1 = mach_time();
145                    timings.push(t1.wrapping_sub(t0));
146                }
147
148                ThreadResult { timings }
149            }));
150        }
151
152        // Start all threads.
153        go.store(1, Ordering::Release);
154
155        // Collect results.
156        let results: Vec<ThreadResult> = handles
157            .into_iter()
158            .map(|h| h.join().unwrap_or(ThreadResult { timings: vec![] }))
159            .collect();
160
161        // Signal stop (in case any thread is still running).
162        stop.store(1, Ordering::Release);
163
164        // XOR-combine timings from all threads for maximum entropy.
165        let min_len = results.iter().map(|r| r.timings.len()).min().unwrap_or(0);
166        if min_len < 4 {
167            return Vec::new();
168        }
169
170        let mut combined: Vec<u64> = Vec::with_capacity(min_len);
171        for i in 0..min_len {
172            let mut val = 0u64;
173            for result in &results {
174                val ^= result.timings[i];
175            }
176            combined.push(val);
177        }
178
179        // Extract entropy: deltas → XOR adjacent → xor-fold.
180        let deltas: Vec<u64> = combined
181            .windows(2)
182            .map(|w| w[1].wrapping_sub(w[0]))
183            .collect();
184        let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
185        let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
186        raw.truncate(n_samples);
187        raw
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn info() {
197        let src = CASContentionSource::default();
198        assert_eq!(src.info().name, "cas_contention");
199        assert!(matches!(src.info().category, SourceCategory::Frontier));
200        assert!(!src.info().composite);
201    }
202
203    #[test]
204    fn custom_config() {
205        let config = CASContentionConfig { num_threads: 2 };
206        let src = CASContentionSource::new(config);
207        assert_eq!(src.config.num_threads, 2);
208    }
209
210    #[test]
211    #[ignore] // Hardware-dependent: requires multi-core CPU
212    fn collects_bytes() {
213        let src = CASContentionSource::default();
214        assert!(src.is_available());
215        let data = src.collect(64);
216        assert!(!data.is_empty());
217        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
218        assert!(unique.len() > 1, "Expected variation in collected bytes");
219    }
220}