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    }
55
56    fn all(&self) -> Vec<EquivocationProof> {
57        self.proofs.clone()
58    }
59}
60
61// ---- Persistent vsdb-backed evidence store (C-3) ----
62
63const META_FILE: &str = "evidence_store.meta";
64
65/// Persistent evidence store backed by vsdb.
66///
67/// Proofs survive node restarts. Uses a `MapxOrd<u64, EquivocationProof>` for
68/// the proof list (keyed by auto-increment ID) and a `MapxOrd<u64, u8>` as a
69/// committed-set (keyed by a hash of (view, validator)).
70pub struct PersistentEvidenceStore {
71    proofs: MapxOrd<u64, EquivocationProof>,
72    committed: MapxOrd<u64, u8>,
73    next_id: u64,
74}
75
76impl PersistentEvidenceStore {
77    /// Open an existing store or create a new one.
78    /// Must be called after `vsdb::vsdb_set_base_dir`.
79    pub fn open(data_dir: &Path) -> Result<Self> {
80        let meta_path = data_dir.join(META_FILE);
81        if meta_path.exists() {
82            let bytes = std::fs::read(&meta_path).c(d!("read evidence_store.meta"))?;
83            if bytes.len() != 24 {
84                return Err(eg!(
85                    "corrupt evidence_store.meta: expected 24 bytes, got {}",
86                    bytes.len()
87                ));
88            }
89            let proofs_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
90            let committed_id = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
91            let next_id = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
92            let proofs = MapxOrd::from_meta(proofs_id).c(d!("restore proofs"))?;
93            let committed = MapxOrd::from_meta(committed_id).c(d!("restore committed"))?;
94            Ok(Self {
95                proofs,
96                committed,
97                next_id,
98            })
99        } else {
100            let proofs: MapxOrd<u64, EquivocationProof> = MapxOrd::new();
101            let committed: MapxOrd<u64, u8> = MapxOrd::new();
102            let proofs_id = proofs.save_meta().c(d!())?;
103            let committed_id = committed.save_meta().c(d!())?;
104            let next_id = 0u64;
105            let mut meta = Vec::with_capacity(24);
106            meta.extend_from_slice(&proofs_id.to_le_bytes());
107            meta.extend_from_slice(&committed_id.to_le_bytes());
108            meta.extend_from_slice(&next_id.to_le_bytes());
109            std::fs::write(&meta_path, &meta).c(d!("write evidence_store.meta"))?;
110            Ok(Self {
111                proofs,
112                committed,
113                next_id,
114            })
115        }
116    }
117
118    fn committed_key(view: ViewNumber, validator: ValidatorId) -> u64 {
119        let mut hasher = blake3::Hasher::new();
120        hasher.update(&view.as_u64().to_le_bytes());
121        hasher.update(&validator.0.to_le_bytes());
122        let hash = hasher.finalize();
123        u64::from_le_bytes(hash.as_bytes()[..8].try_into().unwrap())
124    }
125
126    fn is_duplicate(&self, proof: &EquivocationProof) -> bool {
127        self.proofs
128            .iter()
129            .any(|(_, p)| p.view == proof.view && p.validator == proof.validator)
130    }
131}
132
133impl EvidenceStore for PersistentEvidenceStore {
134    fn put_evidence(&mut self, proof: EquivocationProof) {
135        if self.is_duplicate(&proof) {
136            return;
137        }
138        self.proofs.insert(&self.next_id, &proof);
139        self.next_id += 1;
140    }
141
142    fn get_pending(&self) -> Vec<EquivocationProof> {
143        self.proofs
144            .iter()
145            .filter_map(|(_, p)| {
146                let key = Self::committed_key(p.view, p.validator);
147                if self.committed.get(&key).is_some() {
148                    None
149                } else {
150                    Some(p)
151                }
152            })
153            .collect()
154    }
155
156    fn mark_committed(&mut self, view: ViewNumber, validator: ValidatorId) {
157        let key = Self::committed_key(view, validator);
158        self.committed.insert(&key, &1);
159    }
160
161    fn all(&self) -> Vec<EquivocationProof> {
162        self.proofs.iter().map(|(_, p)| p).collect()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use hotmint_types::block::BlockHash;
170    use hotmint_types::crypto::Signature;
171    use hotmint_types::vote::VoteType;
172
173    fn dummy_proof(view: u64, validator: u64) -> EquivocationProof {
174        EquivocationProof {
175            validator: ValidatorId(validator),
176            view: ViewNumber(view),
177            vote_type: VoteType::Vote,
178            epoch: Default::default(),
179            block_hash_a: BlockHash::GENESIS,
180            signature_a: Signature(vec![1]),
181            block_hash_b: BlockHash::GENESIS,
182            signature_b: Signature(vec![2]),
183        }
184    }
185
186    #[test]
187    fn put_and_get_pending() {
188        let mut store = MemoryEvidenceStore::new();
189        store.put_evidence(dummy_proof(1, 0));
190        store.put_evidence(dummy_proof(2, 1));
191
192        assert_eq!(store.get_pending().len(), 2);
193        assert_eq!(store.all().len(), 2);
194    }
195
196    #[test]
197    fn mark_committed_filters_pending() {
198        let mut store = MemoryEvidenceStore::new();
199        store.put_evidence(dummy_proof(1, 0));
200        store.put_evidence(dummy_proof(2, 1));
201        store.mark_committed(ViewNumber(1), ValidatorId(0));
202
203        let pending = store.get_pending();
204        assert_eq!(pending.len(), 1);
205        assert_eq!(pending[0].view, ViewNumber(2));
206        // all() still returns everything
207        assert_eq!(store.all().len(), 2);
208    }
209
210    #[test]
211    fn deduplication() {
212        let mut store = MemoryEvidenceStore::new();
213        store.put_evidence(dummy_proof(1, 0));
214        store.put_evidence(dummy_proof(1, 0)); // duplicate
215        assert_eq!(store.all().len(), 1);
216    }
217}