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;
14const KEY_PENDING_EPOCH: u64 = 7;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18enum StateValue {
19 View(ViewNumber),
20 Height(Height),
21 Qc(QuorumCertificate),
22 Epoch(Epoch),
23 AppHash(BlockHash),
24}
25
26const META_FILE: &str = "consensus_state.meta";
28
29pub struct PersistentConsensusState {
31 store: MapxOrd<u64, StateValue>,
32}
33
34impl PersistentConsensusState {
35 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 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 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}