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