Skip to main content

clawft_kernel/
chain_rule_hash.rs

1//! Lock-free holder for the current rule hash (ADR-103 A7).
2//!
3//! The chain calls its `rule_hash` provider while holding the chain lock,
4//! so the provider must not take locks. [`RuleHashCell`] is the intended
5//! source: writers publish with [`RuleHashCell::set`], the provider reads
6//! with [`RuleHashCell::get`], and a read never takes a lock (a seqlock
7//! over four atomic words). A reader only retries while a write is in
8//! flight, which is a few instructions; it spins briefly and then yields,
9//! so it is lock-free in the sense that matters here (no mutex, no
10//! priority inversion on the chain lock) but not wait-free.
11
12use std::sync::Mutex;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering, fence};
14
15/// The current rule hash, readable without locking.
16#[derive(Debug, Default)]
17pub struct RuleHashCell {
18    /// Odd while a write is in flight.
19    seq: AtomicU64,
20    present: AtomicBool,
21    words: [AtomicU64; 4],
22    /// Serializes writers only; readers never touch it.
23    writers: Mutex<()>,
24}
25
26impl RuleHashCell {
27    /// An empty cell (`get` returns `None`).
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Publish a new value (or clear it).
33    pub fn set(&self, hash: Option<[u8; 32]>) {
34        let _w = self.writers.lock().unwrap_or_else(|e| e.into_inner());
35        self.seq.fetch_add(1, Ordering::Relaxed); // odd: write in flight
36        fence(Ordering::Release); // order the odd marker before the data stores
37        let h = hash.unwrap_or([0; 32]);
38        for (i, w) in self.words.iter().enumerate() {
39            let mut b = [0u8; 8];
40            b.copy_from_slice(&h[i * 8..i * 8 + 8]);
41            w.store(u64::from_le_bytes(b), Ordering::Relaxed);
42        }
43        self.present.store(hash.is_some(), Ordering::Relaxed);
44        self.seq.fetch_add(1, Ordering::Release); // even: stable
45    }
46
47    /// The current value. Takes no lock; retries (spin, then yield) only
48    /// while a write is in flight.
49    pub fn get(&self) -> Option<[u8; 32]> {
50        let mut spins = 0u32;
51        loop {
52            let s1 = self.seq.load(Ordering::Acquire);
53            if s1 & 1 == 1 {
54                spins += 1;
55                if spins < 16 {
56                    std::hint::spin_loop();
57                } else {
58                    std::thread::yield_now();
59                }
60                continue;
61            }
62            let mut out = [0u8; 32];
63            for (i, w) in self.words.iter().enumerate() {
64                out[i * 8..i * 8 + 8].copy_from_slice(&w.load(Ordering::Relaxed).to_le_bytes());
65            }
66            let present = self.present.load(Ordering::Relaxed);
67            fence(Ordering::Acquire);
68            if self.seq.load(Ordering::Relaxed) == s1 {
69                return present.then_some(out);
70            }
71        }
72    }
73
74    /// A provider closure for `ChainManager::set_rule_hash_provider`.
75    pub fn provider(self: &std::sync::Arc<Self>) -> crate::chain::RuleHashProvider {
76        let cell = std::sync::Arc::clone(self);
77        std::sync::Arc::new(move || cell.get())
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn set_get_clear() {
87        let c = RuleHashCell::new();
88        assert_eq!(c.get(), None);
89        c.set(Some([7; 32]));
90        assert_eq!(c.get(), Some([7; 32]));
91        c.set(None);
92        assert_eq!(c.get(), None);
93    }
94
95    #[test]
96    fn readers_never_see_a_torn_value() {
97        let c = std::sync::Arc::new(RuleHashCell::new());
98        let w = {
99            let c = c.clone();
100            std::thread::spawn(move || {
101                for i in 0..20_000u32 {
102                    c.set(Some([(i % 251) as u8; 32]));
103                }
104            })
105        };
106        for _ in 0..20_000 {
107            if let Some(h) = c.get() {
108                assert!(h.iter().all(|b| *b == h[0]), "torn read {h:?}");
109            }
110        }
111        w.join().unwrap();
112    }
113}