Skip to main content

hotmint_storage/
evidence_store.rs

1use std::collections::HashSet;
2use std::path::Path;
3
4use hotmint_consensus::evidence_store::EvidenceStore;
5use hotmint_types::evidence::EquivocationProof;
6use hotmint_types::validator::ValidatorId;
7use hotmint_types::view::ViewNumber;
8use ruc::*;
9use vsdb::MapxOrd;
10
11/// In-memory evidence store backed by a `Vec` and a committed-set.
12pub struct MemoryEvidenceStore {
13    proofs: Vec<EquivocationProof>,
14    committed: HashSet<(ViewNumber, ValidatorId)>,
15}
16
17impl MemoryEvidenceStore {
18    pub fn new() -> Self {
19        Self {
20            proofs: Vec::new(),
21            committed: HashSet::new(),
22        }
23    }
24}
25
26impl Default for MemoryEvidenceStore {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl EvidenceStore for MemoryEvidenceStore {
33    fn put_evidence(&mut self, proof: EquivocationProof) {
34        // Deduplicate: skip if we already have evidence for this (view, validator).
35        let dominated = self
36            .proofs
37            .iter()
38            .any(|p| p.view == proof.view && p.validator == proof.validator);
39        if !dominated {
40            self.proofs.push(proof);
41        }
42    }
43
44    fn get_pending(&self) -> Vec<EquivocationProof> {
45        self.proofs
46            .iter()
47            .filter(|p| !self.committed.contains(&(p.view, p.validator)))
48            .cloned()
49            .collect()
50    }
51
52    fn mark_committed(&mut self, view: ViewNumber, validator: ValidatorId) {
53        self.committed.insert((view, validator));
54        // C-5: Prune committed proofs to avoid unbounded growth.
55        self.proofs
56            .retain(|p| !self.committed.contains(&(p.view, p.validator)));
57    }
58
59    fn all(&self) -> Vec<EquivocationProof> {
60        self.proofs.clone()
61    }
62}
63
64// ---- Persistent vsdb-backed evidence store (C-3) ----
65
66const META_FILE: &str = "evidence_store.meta";
67
68/// Persistent evidence store backed by vsdb.
69///
70/// Proofs survive node restarts. Uses a `MapxOrd<u64, EquivocationProof>` for
71/// the proof list (keyed by auto-increment ID) and a `MapxOrd<u64, u8>` as a
72/// committed-set (keyed by a hash of (view, validator)).
73pub struct PersistentEvidenceStore {
74    proofs: MapxOrd<u64, EquivocationProof>,
75    committed: MapxOrd<u64, u8>,
76    next_id: u64,
77    meta_path: std::path::PathBuf,
78}
79
80impl PersistentEvidenceStore {
81    /// Open an existing store or create a new one.
82    /// Must be called after `vsdb::vsdb_set_base_dir`.
83    pub fn open(data_dir: &Path) -> Result<Self> {
84        let meta_path = data_dir.join(META_FILE);
85        if meta_path.exists() {
86            let bytes = std::fs::read(&meta_path).c(d!("read evidence_store.meta"))?;
87            if bytes.len() != 24 {
88                return Err(eg!(
89                    "corrupt evidence_store.meta: expected 24 bytes, got {}",
90                    bytes.len()
91                ));
92            }
93            let proofs_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
94            let committed_id = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
95            let next_id = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
96            let proofs = MapxOrd::from_meta(proofs_id).c(d!("restore proofs"))?;
97            let committed = MapxOrd::from_meta(committed_id).c(d!("restore committed"))?;
98            Ok(Self {
99                proofs,
100                committed,
101                next_id,
102                meta_path: meta_path.clone(),
103            })
104        } else {
105            let proofs: MapxOrd<u64, EquivocationProof> = MapxOrd::new();
106            let committed: MapxOrd<u64, u8> = MapxOrd::new();
107            let proofs_id = proofs.save_meta().c(d!())?;
108            let committed_id = committed.save_meta().c(d!())?;
109            let next_id = 0u64;
110            let mut meta = Vec::with_capacity(24);
111            meta.extend_from_slice(&proofs_id.to_le_bytes());
112            meta.extend_from_slice(&committed_id.to_le_bytes());
113            meta.extend_from_slice(&next_id.to_le_bytes());
114            std::fs::write(&meta_path, &meta).c(d!("write evidence_store.meta"))?;
115            Ok(Self {
116                proofs,
117                committed,
118                next_id,
119                meta_path,
120            })
121        }
122    }
123
124    fn committed_key(view: ViewNumber, validator: ValidatorId) -> u64 {
125        let mut hasher = blake3::Hasher::new();
126        hasher.update(&view.as_u64().to_le_bytes());
127        hasher.update(&validator.0.to_le_bytes());
128        let hash = hasher.finalize();
129        u64::from_le_bytes(hash.as_bytes()[..8].try_into().unwrap())
130    }
131
132    fn is_duplicate(&self, proof: &EquivocationProof) -> bool {
133        self.proofs
134            .iter()
135            .any(|(_, p)| p.view == proof.view && p.validator == proof.validator)
136    }
137
138    /// A-4: Write the updated next_id back to the meta file so it survives restarts.
139    fn persist_next_id(&self) {
140        if let Ok(bytes) = std::fs::read(&self.meta_path)
141            && bytes.len() == 24
142        {
143            let mut meta = bytes;
144            meta[16..24].copy_from_slice(&self.next_id.to_le_bytes());
145            let _ = std::fs::write(&self.meta_path, &meta);
146        }
147    }
148}
149
150impl EvidenceStore for PersistentEvidenceStore {
151    fn put_evidence(&mut self, proof: EquivocationProof) {
152        if self.is_duplicate(&proof) {
153            return;
154        }
155        self.proofs.insert(&self.next_id, &proof);
156        self.next_id += 1;
157        // A-4: Persist next_id so it survives restarts.
158        self.persist_next_id();
159    }
160
161    fn get_pending(&self) -> Vec<EquivocationProof> {
162        self.proofs
163            .iter()
164            .filter_map(|(_, p)| {
165                let key = Self::committed_key(p.view, p.validator);
166                if self.committed.get(&key).is_some() {
167                    None
168                } else {
169                    Some(p)
170                }
171            })
172            .collect()
173    }
174
175    fn mark_committed(&mut self, view: ViewNumber, validator: ValidatorId) {
176        let key = Self::committed_key(view, validator);
177        self.committed.insert(&key, &1);
178        // C-5: Prune the committed proof from the proofs map to bound growth.
179        let to_remove: Vec<u64> = self
180            .proofs
181            .iter()
182            .filter(|(_, p)| p.view == view && p.validator == validator)
183            .map(|(id, _)| id)
184            .collect();
185        for id in to_remove {
186            self.proofs.remove(&id);
187        }
188    }
189
190    fn all(&self) -> Vec<EquivocationProof> {
191        self.proofs.iter().map(|(_, p)| p).collect()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use hotmint_types::block::BlockHash;
199    use hotmint_types::crypto::Signature;
200    use hotmint_types::vote::VoteType;
201
202    fn dummy_proof(view: u64, validator: u64) -> EquivocationProof {
203        EquivocationProof {
204            validator: ValidatorId(validator),
205            view: ViewNumber(view),
206            vote_type: VoteType::Vote,
207            epoch: Default::default(),
208            block_hash_a: BlockHash::GENESIS,
209            signature_a: Signature(vec![1]),
210            block_hash_b: BlockHash::GENESIS,
211            signature_b: Signature(vec![2]),
212        }
213    }
214
215    #[test]
216    fn put_and_get_pending() {
217        let mut store = MemoryEvidenceStore::new();
218        store.put_evidence(dummy_proof(1, 0));
219        store.put_evidence(dummy_proof(2, 1));
220
221        assert_eq!(store.get_pending().len(), 2);
222        assert_eq!(store.all().len(), 2);
223    }
224
225    #[test]
226    fn mark_committed_filters_pending() {
227        let mut store = MemoryEvidenceStore::new();
228        store.put_evidence(dummy_proof(1, 0));
229        store.put_evidence(dummy_proof(2, 1));
230        store.mark_committed(ViewNumber(1), ValidatorId(0));
231
232        let pending = store.get_pending();
233        assert_eq!(pending.len(), 1);
234        assert_eq!(pending[0].view, ViewNumber(2));
235        // C-5: committed proofs are pruned, so all() returns only the remaining one
236        assert_eq!(store.all().len(), 1);
237    }
238
239    #[test]
240    fn deduplication() {
241        let mut store = MemoryEvidenceStore::new();
242        store.put_evidence(dummy_proof(1, 0));
243        store.put_evidence(dummy_proof(1, 0)); // duplicate
244        assert_eq!(store.all().len(), 1);
245    }
246}