Skip to main content

hotmint_storage/
consensus_state.rs

1use hotmint_types::{BlockHash, Epoch, Height, QuorumCertificate, ViewNumber};
2use ruc::*;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use vsdb::MapxOrd;
6
7/// Key constants for the consensus state KV store
8const KEY_CURRENT_VIEW: u64 = 1;
9const KEY_LOCKED_QC: u64 = 2;
10const KEY_HIGHEST_QC: u64 = 3;
11const KEY_LAST_COMMITTED_HEIGHT: u64 = 4;
12const KEY_CURRENT_EPOCH: u64 = 5;
13const KEY_LAST_APP_HASH: u64 = 6;
14
15/// Persisted consensus state fields (serialized as a single blob per key)
16#[derive(Debug, Clone, Serialize, Deserialize)]
17enum StateValue {
18    View(ViewNumber),
19    Height(Height),
20    Qc(QuorumCertificate),
21    Epoch(Epoch),
22    AppHash(BlockHash),
23}
24
25/// File name for the persisted instance ID of the consensus state collection.
26const META_FILE: &str = "consensus_state.meta";
27
28/// Persistent consensus state store backed by vsdb
29pub struct PersistentConsensusState {
30    store: MapxOrd<u64, StateValue>,
31}
32
33impl PersistentConsensusState {
34    /// Opens an existing consensus state store or creates a fresh one.
35    ///
36    /// Must be called after [`vsdb::vsdb_set_base_dir`].
37    /// The instance ID of the internal collection is stored in
38    /// `data_dir/consensus_state.meta` (8 bytes: one little-endian u64).
39    /// On first run the file is created; on subsequent runs the collection
40    /// is recovered via [`MapxOrd::from_meta`].
41    pub fn open(data_dir: &Path) -> Result<Self> {
42        let meta_path = data_dir.join(META_FILE);
43        if meta_path.exists() {
44            let bytes = std::fs::read(&meta_path).c(d!("read consensus_state.meta"))?;
45            if bytes.len() != 8 {
46                return Err(eg!(
47                    "corrupt consensus_state.meta: expected 8 bytes, got {}",
48                    bytes.len()
49                ));
50            }
51            let store_id = u64::from_le_bytes(bytes.try_into().unwrap());
52            Ok(Self {
53                store: MapxOrd::from_meta(store_id).c(d!("restore consensus store"))?,
54            })
55        } else {
56            let store: MapxOrd<u64, StateValue> = MapxOrd::new();
57            let store_id = store.save_meta().c(d!())?;
58            std::fs::write(&meta_path, store_id.to_le_bytes())
59                .c(d!("write consensus_state.meta"))?;
60            Ok(Self { store })
61        }
62    }
63
64    /// Creates a new in-memory consensus state without any persistent meta file.
65    /// Intended for unit tests only; use [`Self::open`] in production.
66    pub fn new() -> Self {
67        Self {
68            store: MapxOrd::new(),
69        }
70    }
71
72    pub fn save_current_view(&mut self, view: ViewNumber) {
73        self.store
74            .insert(&KEY_CURRENT_VIEW, &StateValue::View(view));
75    }
76
77    pub fn load_current_view(&self) -> Option<ViewNumber> {
78        self.store.get(&KEY_CURRENT_VIEW).and_then(|v| match v {
79            StateValue::View(view) => Some(view),
80            _ => None,
81        })
82    }
83
84    pub fn save_locked_qc(&mut self, qc: &QuorumCertificate) {
85        self.store
86            .insert(&KEY_LOCKED_QC, &StateValue::Qc(qc.clone()));
87    }
88
89    pub fn load_locked_qc(&self) -> Option<QuorumCertificate> {
90        self.store.get(&KEY_LOCKED_QC).and_then(|v| match v {
91            StateValue::Qc(qc) => Some(qc),
92            _ => None,
93        })
94    }
95
96    pub fn save_highest_qc(&mut self, qc: &QuorumCertificate) {
97        self.store
98            .insert(&KEY_HIGHEST_QC, &StateValue::Qc(qc.clone()));
99    }
100
101    pub fn load_highest_qc(&self) -> Option<QuorumCertificate> {
102        self.store.get(&KEY_HIGHEST_QC).and_then(|v| match v {
103            StateValue::Qc(qc) => Some(qc),
104            _ => None,
105        })
106    }
107
108    pub fn save_last_committed_height(&mut self, height: Height) {
109        self.store
110            .insert(&KEY_LAST_COMMITTED_HEIGHT, &StateValue::Height(height));
111    }
112
113    pub fn load_last_committed_height(&self) -> Option<Height> {
114        self.store
115            .get(&KEY_LAST_COMMITTED_HEIGHT)
116            .and_then(|v| match v {
117                StateValue::Height(h) => Some(h),
118                _ => None,
119            })
120    }
121
122    pub fn save_current_epoch(&mut self, epoch: &Epoch) {
123        self.store
124            .insert(&KEY_CURRENT_EPOCH, &StateValue::Epoch(epoch.clone()));
125    }
126
127    pub fn load_current_epoch(&self) -> Option<Epoch> {
128        self.store.get(&KEY_CURRENT_EPOCH).and_then(|v| match v {
129            StateValue::Epoch(e) => Some(e),
130            _ => None,
131        })
132    }
133
134    pub fn save_last_app_hash(&mut self, hash: BlockHash) {
135        self.store
136            .insert(&KEY_LAST_APP_HASH, &StateValue::AppHash(hash));
137    }
138
139    pub fn load_last_app_hash(&self) -> Option<BlockHash> {
140        self.store.get(&KEY_LAST_APP_HASH).and_then(|v| match v {
141            StateValue::AppHash(h) => Some(h),
142            _ => None,
143        })
144    }
145
146    pub fn flush(&self) {
147        vsdb::vsdb_flush();
148    }
149}
150
151impl hotmint_consensus::engine::StatePersistence for PersistentConsensusState {
152    fn save_current_view(&mut self, view: ViewNumber) {
153        self.save_current_view(view);
154    }
155    fn save_locked_qc(&mut self, qc: &QuorumCertificate) {
156        self.save_locked_qc(qc);
157    }
158    fn save_highest_qc(&mut self, qc: &QuorumCertificate) {
159        self.save_highest_qc(qc);
160    }
161    fn save_last_committed_height(&mut self, height: Height) {
162        self.save_last_committed_height(height);
163    }
164    fn save_current_epoch(&mut self, epoch: &Epoch) {
165        self.save_current_epoch(epoch);
166    }
167    fn save_last_app_hash(&mut self, hash: BlockHash) {
168        self.save_last_app_hash(hash);
169    }
170    fn flush(&self) {
171        self.flush();
172    }
173}
174
175impl Default for PersistentConsensusState {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use hotmint_types::{BlockHash, Height, ViewNumber};
185
186    #[test]
187    fn last_app_hash_round_trips() {
188        let mut pcs = PersistentConsensusState::new();
189        assert_eq!(pcs.load_last_app_hash(), None);
190
191        let hash = BlockHash([0xAB; 32]);
192        pcs.save_last_app_hash(hash);
193        assert_eq!(pcs.load_last_app_hash(), Some(hash));
194
195        // Overwrite with a different value
196        let hash2 = BlockHash([0xCD; 32]);
197        pcs.save_last_app_hash(hash2);
198        assert_eq!(pcs.load_last_app_hash(), Some(hash2));
199    }
200
201    #[test]
202    fn all_fields_independent() {
203        let mut pcs = PersistentConsensusState::new();
204        pcs.save_current_view(ViewNumber(42));
205        pcs.save_last_committed_height(Height(7));
206        pcs.save_last_app_hash(BlockHash([0xFF; 32]));
207
208        assert_eq!(pcs.load_current_view(), Some(ViewNumber(42)));
209        assert_eq!(pcs.load_last_committed_height(), Some(Height(7)));
210        assert_eq!(pcs.load_last_app_hash(), Some(BlockHash([0xFF; 32])));
211    }
212}