Skip to main content

hotmint_storage/
consensus_state.rs

1use hotmint_types::{BlockHash, Epoch, Height, QuorumCertificate, ViewNumber};
2use serde::{Deserialize, Serialize};
3use vsdb::MapxOrd;
4
5/// Key constants for the consensus state KV store
6const KEY_CURRENT_VIEW: u64 = 1;
7const KEY_LOCKED_QC: u64 = 2;
8const KEY_HIGHEST_QC: u64 = 3;
9const KEY_LAST_COMMITTED_HEIGHT: u64 = 4;
10const KEY_CURRENT_EPOCH: u64 = 5;
11const KEY_LAST_APP_HASH: u64 = 6;
12
13/// Persisted consensus state fields (serialized as a single blob per key)
14#[derive(Debug, Clone, Serialize, Deserialize)]
15enum StateValue {
16    View(ViewNumber),
17    Height(Height),
18    Qc(QuorumCertificate),
19    Epoch(Epoch),
20    AppHash(BlockHash),
21}
22
23/// Persistent consensus state store backed by vsdb
24pub struct PersistentConsensusState {
25    store: MapxOrd<u64, StateValue>,
26}
27
28impl PersistentConsensusState {
29    pub fn new() -> Self {
30        Self {
31            store: MapxOrd::new(),
32        }
33    }
34
35    pub fn save_current_view(&mut self, view: ViewNumber) {
36        self.store
37            .insert(&KEY_CURRENT_VIEW, &StateValue::View(view));
38    }
39
40    pub fn load_current_view(&self) -> Option<ViewNumber> {
41        self.store.get(&KEY_CURRENT_VIEW).and_then(|v| match v {
42            StateValue::View(view) => Some(view),
43            _ => None,
44        })
45    }
46
47    pub fn save_locked_qc(&mut self, qc: &QuorumCertificate) {
48        self.store
49            .insert(&KEY_LOCKED_QC, &StateValue::Qc(qc.clone()));
50    }
51
52    pub fn load_locked_qc(&self) -> Option<QuorumCertificate> {
53        self.store.get(&KEY_LOCKED_QC).and_then(|v| match v {
54            StateValue::Qc(qc) => Some(qc),
55            _ => None,
56        })
57    }
58
59    pub fn save_highest_qc(&mut self, qc: &QuorumCertificate) {
60        self.store
61            .insert(&KEY_HIGHEST_QC, &StateValue::Qc(qc.clone()));
62    }
63
64    pub fn load_highest_qc(&self) -> Option<QuorumCertificate> {
65        self.store.get(&KEY_HIGHEST_QC).and_then(|v| match v {
66            StateValue::Qc(qc) => Some(qc),
67            _ => None,
68        })
69    }
70
71    pub fn save_last_committed_height(&mut self, height: Height) {
72        self.store
73            .insert(&KEY_LAST_COMMITTED_HEIGHT, &StateValue::Height(height));
74    }
75
76    pub fn load_last_committed_height(&self) -> Option<Height> {
77        self.store
78            .get(&KEY_LAST_COMMITTED_HEIGHT)
79            .and_then(|v| match v {
80                StateValue::Height(h) => Some(h),
81                _ => None,
82            })
83    }
84
85    pub fn save_current_epoch(&mut self, epoch: &Epoch) {
86        self.store
87            .insert(&KEY_CURRENT_EPOCH, &StateValue::Epoch(epoch.clone()));
88    }
89
90    pub fn load_current_epoch(&self) -> Option<Epoch> {
91        self.store.get(&KEY_CURRENT_EPOCH).and_then(|v| match v {
92            StateValue::Epoch(e) => Some(e),
93            _ => None,
94        })
95    }
96
97    pub fn save_last_app_hash(&mut self, hash: BlockHash) {
98        self.store
99            .insert(&KEY_LAST_APP_HASH, &StateValue::AppHash(hash));
100    }
101
102    pub fn load_last_app_hash(&self) -> Option<BlockHash> {
103        self.store.get(&KEY_LAST_APP_HASH).and_then(|v| match v {
104            StateValue::AppHash(h) => Some(h),
105            _ => None,
106        })
107    }
108
109    pub fn flush(&self) {
110        vsdb::vsdb_flush();
111    }
112}
113
114impl hotmint_consensus::engine::StatePersistence for PersistentConsensusState {
115    fn save_current_view(&mut self, view: ViewNumber) {
116        self.save_current_view(view);
117    }
118    fn save_locked_qc(&mut self, qc: &QuorumCertificate) {
119        self.save_locked_qc(qc);
120    }
121    fn save_highest_qc(&mut self, qc: &QuorumCertificate) {
122        self.save_highest_qc(qc);
123    }
124    fn save_last_committed_height(&mut self, height: Height) {
125        self.save_last_committed_height(height);
126    }
127    fn save_current_epoch(&mut self, epoch: &Epoch) {
128        self.save_current_epoch(epoch);
129    }
130    fn save_last_app_hash(&mut self, hash: BlockHash) {
131        self.save_last_app_hash(hash);
132    }
133    fn flush(&self) {
134        self.flush();
135    }
136}
137
138impl Default for PersistentConsensusState {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use hotmint_types::{BlockHash, Height, ViewNumber};
148
149    #[test]
150    fn last_app_hash_round_trips() {
151        let mut pcs = PersistentConsensusState::new();
152        assert_eq!(pcs.load_last_app_hash(), None);
153
154        let hash = BlockHash([0xAB; 32]);
155        pcs.save_last_app_hash(hash);
156        assert_eq!(pcs.load_last_app_hash(), Some(hash));
157
158        // Overwrite with a different value
159        let hash2 = BlockHash([0xCD; 32]);
160        pcs.save_last_app_hash(hash2);
161        assert_eq!(pcs.load_last_app_hash(), Some(hash2));
162    }
163
164    #[test]
165    fn all_fields_independent() {
166        let mut pcs = PersistentConsensusState::new();
167        pcs.save_current_view(ViewNumber(42));
168        pcs.save_last_committed_height(Height(7));
169        pcs.save_last_app_hash(BlockHash([0xFF; 32]));
170
171        assert_eq!(pcs.load_current_view(), Some(ViewNumber(42)));
172        assert_eq!(pcs.load_last_committed_height(), Some(Height(7)));
173        assert_eq!(pcs.load_last_app_hash(), Some(BlockHash([0xFF; 32])));
174    }
175}