hotmint_storage/
consensus_state.rs1use hotmint_types::{BlockHash, Epoch, Height, QuorumCertificate, ViewNumber};
2use ruc::*;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use vsdb::MapxOrd;
6
7const 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#[derive(Debug, Clone, Serialize, Deserialize)]
17enum StateValue {
18 View(ViewNumber),
19 Height(Height),
20 Qc(QuorumCertificate),
21 Epoch(Epoch),
22 AppHash(BlockHash),
23}
24
25const META_FILE: &str = "consensus_state.meta";
27
28pub struct PersistentConsensusState {
30 store: MapxOrd<u64, StateValue>,
31}
32
33impl PersistentConsensusState {
34 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 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 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}