1use crate::error::{ConsensusError, Result};
31use crate::voter::VoteType;
32use serde::{Deserialize, Serialize};
33use std::path::{Path, PathBuf};
34use std::sync::Arc;
35use tenzro_types::primitives::Hash;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44#[repr(u8)]
45pub enum VoteStep {
46 Prepare = 1,
47 Commit = 2,
48 Decide = 3,
49}
50
51impl VoteStep {
52 pub fn from_vote_type(vt: VoteType) -> Self {
53 match vt {
54 VoteType::Prepare => VoteStep::Prepare,
55 VoteType::Commit => VoteStep::Commit,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct LastSignState {
66 #[serde(default = "default_version")]
68 pub version: u8,
69
70 pub view: u64,
72
73 pub height: u64,
75
76 pub step: VoteStep,
78
79 #[serde(default)]
83 pub block_hash: Option<Hash>,
84
85 #[serde(default)]
91 pub signature: Option<Vec<u8>>,
92}
93
94fn default_version() -> u8 {
95 1
96}
97
98impl LastSignState {
99 pub fn empty() -> Self {
101 Self {
102 version: 1,
103 view: 0,
104 height: 0,
105 step: VoteStep::Prepare,
106 block_hash: None,
107 signature: None,
108 }
109 }
110
111 pub fn is_strictly_after(&self, view: u64, height: u64, step: VoteStep) -> bool {
119 (view, height, step) > (self.view, self.height, self.step)
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum VrsDecision {
126 Sign,
128 Reuse {
131 signature: Vec<u8>,
132 },
133 Reject {
135 reason: String,
136 },
137}
138
139pub trait VoteStateStore: Send + Sync {
148 fn load(&self) -> Result<LastSignState>;
151
152 fn record(&self, state: &LastSignState) -> Result<()>;
154
155 fn check_vrs(
161 &self,
162 view: u64,
163 height: u64,
164 step: VoteStep,
165 block_hash: &Hash,
166 ) -> Result<VrsDecision> {
167 let last = self.load()?;
168
169 if last.is_strictly_after(view, height, step) {
171 return Ok(VrsDecision::Sign);
172 }
173
174 if last.view == view
176 && last.height == height
177 && last.step == step
178 && last.block_hash.as_ref() == Some(block_hash)
179 && let Some(sig) = last.signature.clone()
180 {
181 return Ok(VrsDecision::Reuse { signature: sig });
182 }
183
184 Ok(VrsDecision::Reject {
186 reason: format!(
187 "double-sign refused: candidate (view={}, height={}, step={:?}, hash={}) \
188 conflicts with last (view={}, height={}, step={:?}, hash={:?})",
189 view,
190 height,
191 step,
192 block_hash,
193 last.view,
194 last.height,
195 last.step,
196 last.block_hash,
197 ),
198 })
199 }
200}
201
202pub struct MemoryVoteStateStore {
206 inner: parking_lot::Mutex<LastSignState>,
207}
208
209impl MemoryVoteStateStore {
210 pub fn new() -> Self {
211 Self {
212 inner: parking_lot::Mutex::new(LastSignState::empty()),
213 }
214 }
215}
216
217impl Default for MemoryVoteStateStore {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl VoteStateStore for MemoryVoteStateStore {
224 fn load(&self) -> Result<LastSignState> {
225 Ok(self.inner.lock().clone())
226 }
227
228 fn record(&self, state: &LastSignState) -> Result<()> {
229 let mut guard = self.inner.lock();
230 *guard = state.clone();
231 Ok(())
232 }
233}
234
235pub struct FileVoteStateStore {
240 path: PathBuf,
241 parent_dir: PathBuf,
242 inner: parking_lot::Mutex<LastSignState>,
243}
244
245impl FileVoteStateStore {
246 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
252 let path = path.as_ref().to_path_buf();
253 let parent_dir = path
254 .parent()
255 .ok_or_else(|| {
256 ConsensusError::Internal(format!(
257 "FileVoteStateStore: path {:?} has no parent directory",
258 path
259 ))
260 })?
261 .to_path_buf();
262
263 let initial = if path.exists() {
264 let bytes = std::fs::read(&path).map_err(|e| {
265 ConsensusError::Internal(format!(
266 "FileVoteStateStore: failed to read {:?}: {}",
267 path, e
268 ))
269 })?;
270 if bytes.is_empty() {
272 LastSignState::empty()
273 } else {
274 serde_json::from_slice::<LastSignState>(&bytes).map_err(|e| {
275 ConsensusError::Internal(format!(
276 "FileVoteStateStore: corrupt state at {:?}: {}",
277 path, e
278 ))
279 })?
280 }
281 } else {
282 LastSignState::empty()
283 };
284
285 Ok(Self {
286 path,
287 parent_dir,
288 inner: parking_lot::Mutex::new(initial),
289 })
290 }
291
292 fn write_atomic(&self, state: &LastSignState) -> Result<()> {
294 use std::io::Write;
295
296 let bytes = serde_json::to_vec_pretty(state).map_err(|e| {
297 ConsensusError::Internal(format!(
298 "FileVoteStateStore: serialize failed: {}",
299 e
300 ))
301 })?;
302
303 let tmp_path = self.path.with_extension("tmp");
304
305 {
307 let mut tmp = std::fs::OpenOptions::new()
308 .write(true)
309 .create(true)
310 .truncate(true)
311 .open(&tmp_path)
312 .map_err(|e| {
313 ConsensusError::Internal(format!(
314 "FileVoteStateStore: open tmp {:?}: {}",
315 tmp_path, e
316 ))
317 })?;
318 tmp.write_all(&bytes).map_err(|e| {
319 ConsensusError::Internal(format!(
320 "FileVoteStateStore: write tmp {:?}: {}",
321 tmp_path, e
322 ))
323 })?;
324 tmp.sync_all().map_err(|e| {
325 ConsensusError::Internal(format!(
326 "FileVoteStateStore: fsync tmp {:?}: {}",
327 tmp_path, e
328 ))
329 })?;
330 }
331
332 std::fs::rename(&tmp_path, &self.path).map_err(|e| {
334 ConsensusError::Internal(format!(
335 "FileVoteStateStore: rename {:?} -> {:?}: {}",
336 tmp_path, self.path, e
337 ))
338 })?;
339
340 if let Ok(dir) = std::fs::File::open(&self.parent_dir) {
344 let _ = dir.sync_all();
348 }
349
350 Ok(())
351 }
352}
353
354impl VoteStateStore for FileVoteStateStore {
355 fn load(&self) -> Result<LastSignState> {
356 Ok(self.inner.lock().clone())
357 }
358
359 fn record(&self, state: &LastSignState) -> Result<()> {
360 let mut guard = self.inner.lock();
361 self.write_atomic(state)?;
364 *guard = state.clone();
365 Ok(())
366 }
367}
368
369pub fn open_default_file_store(data_dir: &Path) -> Result<Arc<dyn VoteStateStore>> {
374 let consensus_dir = data_dir.join("consensus");
375 std::fs::create_dir_all(&consensus_dir).map_err(|e| {
376 ConsensusError::Internal(format!(
377 "open_default_file_store: create_dir_all {:?}: {}",
378 consensus_dir, e
379 ))
380 })?;
381 let path = consensus_dir.join("last_sign.json");
382 let store = FileVoteStateStore::open(path)?;
383 Ok(Arc::new(store))
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use tempfile::TempDir;
390
391 fn h(b: u8) -> Hash {
392 Hash::new([b; 32])
393 }
394
395 #[test]
396 fn empty_state_allows_anything() {
397 let s = LastSignState::empty();
398 assert!(s.is_strictly_after(0, 1, VoteStep::Prepare));
399 assert!(s.is_strictly_after(1, 0, VoteStep::Prepare));
400 }
401
402 #[test]
403 fn lexicographic_ordering() {
404 let s = LastSignState {
405 version: 1,
406 view: 5,
407 height: 10,
408 step: VoteStep::Prepare,
409 block_hash: Some(h(1)),
410 signature: Some(vec![0xAA]),
411 };
412
413 assert!(s.is_strictly_after(5, 10, VoteStep::Commit));
415 assert!(!s.is_strictly_after(5, 10, VoteStep::Prepare));
417 assert!(!s.is_strictly_after(4, 10, VoteStep::Decide));
419 assert!(!s.is_strictly_after(5, 9, VoteStep::Decide));
421 assert!(s.is_strictly_after(6, 0, VoteStep::Prepare));
423 }
424
425 #[test]
426 fn memory_store_sign_then_reject_double_sign() {
427 let store = MemoryVoteStateStore::new();
428
429 match store.check_vrs(10, 42, VoteStep::Prepare, &h(1)).unwrap() {
431 VrsDecision::Sign => {}
432 other => panic!("expected Sign, got {:?}", other),
433 }
434
435 store
437 .record(&LastSignState {
438 version: 1,
439 view: 10,
440 height: 42,
441 step: VoteStep::Prepare,
442 block_hash: Some(h(1)),
443 signature: Some(vec![0xDE, 0xAD]),
444 })
445 .unwrap();
446
447 match store.check_vrs(10, 42, VoteStep::Prepare, &h(2)).unwrap() {
450 VrsDecision::Reject { .. } => {}
451 other => panic!("expected Reject, got {:?}", other),
452 }
453
454 match store.check_vrs(10, 42, VoteStep::Prepare, &h(1)).unwrap() {
456 VrsDecision::Reuse { signature } => assert_eq!(signature, vec![0xDE, 0xAD]),
457 other => panic!("expected Reuse, got {:?}", other),
458 }
459
460 match store.check_vrs(9, 99, VoteStep::Decide, &h(3)).unwrap() {
462 VrsDecision::Reject { .. } => {}
463 other => panic!("expected Reject for earlier view, got {:?}", other),
464 }
465
466 match store.check_vrs(11, 0, VoteStep::Prepare, &h(4)).unwrap() {
468 VrsDecision::Sign => {}
469 other => panic!("expected Sign for later view, got {:?}", other),
470 }
471 }
472
473 #[test]
474 fn memory_store_advancing_step_within_same_view() {
475 let store = MemoryVoteStateStore::new();
476 store
477 .record(&LastSignState {
478 version: 1,
479 view: 7,
480 height: 21,
481 step: VoteStep::Prepare,
482 block_hash: Some(h(9)),
483 signature: Some(vec![1, 2, 3]),
484 })
485 .unwrap();
486
487 match store.check_vrs(7, 21, VoteStep::Commit, &h(9)).unwrap() {
489 VrsDecision::Sign => {}
490 other => panic!("expected Sign for Commit advance, got {:?}", other),
491 }
492 }
493
494 #[test]
495 fn file_store_round_trip() {
496 let tmp = TempDir::new().unwrap();
497 let path = tmp.path().join("last_sign.json");
498
499 let store = FileVoteStateStore::open(&path).unwrap();
500 store
501 .record(&LastSignState {
502 version: 1,
503 view: 12,
504 height: 100,
505 step: VoteStep::Commit,
506 block_hash: Some(h(7)),
507 signature: Some(vec![0xCA, 0xFE]),
508 })
509 .unwrap();
510
511 let store2 = FileVoteStateStore::open(&path).unwrap();
513 let loaded = store2.load().unwrap();
514 assert_eq!(loaded.view, 12);
515 assert_eq!(loaded.height, 100);
516 assert_eq!(loaded.step, VoteStep::Commit);
517 assert_eq!(loaded.block_hash, Some(h(7)));
518 assert_eq!(loaded.signature, Some(vec![0xCA, 0xFE]));
519 }
520
521 #[test]
522 fn file_store_rejects_replay_after_restart() {
523 let tmp = TempDir::new().unwrap();
524 let path = tmp.path().join("last_sign.json");
525
526 let store = FileVoteStateStore::open(&path).unwrap();
527 store
528 .record(&LastSignState {
529 version: 1,
530 view: 50,
531 height: 200,
532 step: VoteStep::Prepare,
533 block_hash: Some(h(1)),
534 signature: Some(vec![0xAB]),
535 })
536 .unwrap();
537
538 let store2 = FileVoteStateStore::open(&path).unwrap();
540
541 match store2.check_vrs(50, 200, VoteStep::Prepare, &h(2)).unwrap() {
543 VrsDecision::Reject { reason } => {
544 assert!(reason.contains("double-sign refused"), "reason: {}", reason);
545 }
546 other => panic!("expected Reject after restart, got {:?}", other),
547 }
548 }
549
550 #[test]
551 fn file_store_handles_corrupt_file_via_error() {
552 let tmp = TempDir::new().unwrap();
553 let path = tmp.path().join("last_sign.json");
554 std::fs::write(&path, b"not valid json{{{").unwrap();
555
556 let result = FileVoteStateStore::open(&path);
557 assert!(result.is_err(), "expected error opening corrupt file");
558 }
559
560 #[test]
561 fn file_store_handles_empty_file_as_fresh() {
562 let tmp = TempDir::new().unwrap();
563 let path = tmp.path().join("last_sign.json");
564 std::fs::write(&path, b"").unwrap();
565
566 let store = FileVoteStateStore::open(&path).unwrap();
567 let loaded = store.load().unwrap();
568 assert_eq!(loaded, LastSignState::empty());
569 }
570}