Skip to main content

reifydb_codec/log/
vote.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use crc32fast::Hasher;
5
6use crate::log::{LogIndex, NodeId, Term, VoteSeq};
7
8pub const SLOT_BYTES: usize = 56;
9
10pub const SLOTS: usize = 2;
11
12pub const FILE_BYTES: usize = SLOT_BYTES;
13
14pub const MAGIC: u32 = u32::from_le_bytes(*b"RVOT");
15
16pub const NONE: u64 = u64::MAX;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct State {
20	pub term: Term,
21	pub voted_for: Option<NodeId>,
22	pub commit_index: LogIndex,
23	pub snapshot_index: LogIndex,
24	pub snapshot_term: Term,
25}
26
27impl State {
28	pub const EMPTY: Self = Self {
29		term: Term::ZERO,
30		voted_for: None,
31		commit_index: LogIndex::ZERO,
32		snapshot_index: LogIndex::ZERO,
33		snapshot_term: Term::ZERO,
34	};
35
36	pub fn encode(&self, seq: VoteSeq) -> [u8; SLOT_BYTES] {
37		let mut out = [0u8; SLOT_BYTES];
38		out[0..4].copy_from_slice(&MAGIC.to_le_bytes());
39		out[8..16].copy_from_slice(&seq.as_u64().to_le_bytes());
40		out[16..24].copy_from_slice(&self.term.as_u64().to_le_bytes());
41		out[24..32].copy_from_slice(&voted_for_bits(self.voted_for).to_le_bytes());
42		out[32..40].copy_from_slice(&self.commit_index.as_u64().to_le_bytes());
43		out[40..48].copy_from_slice(&self.snapshot_index.as_u64().to_le_bytes());
44		out[48..56].copy_from_slice(&self.snapshot_term.as_u64().to_le_bytes());
45		let checksum = checksum(&out[8..]);
46		out[4..8].copy_from_slice(&checksum.to_le_bytes());
47		out
48	}
49
50	pub fn decode(buf: &[u8; SLOT_BYTES]) -> Option<(Self, VoteSeq)> {
51		if u32::from_le_bytes(buf[0..4].try_into().unwrap()) != MAGIC {
52			return None;
53		}
54		if u32::from_le_bytes(buf[4..8].try_into().unwrap()) != checksum(&buf[8..]) {
55			return None;
56		}
57		let state = Self {
58			term: Term::new(u64::from_le_bytes(buf[16..24].try_into().unwrap())),
59			voted_for: voted_for_of(u64::from_le_bytes(buf[24..32].try_into().unwrap())),
60			commit_index: LogIndex::new(u64::from_le_bytes(buf[32..40].try_into().unwrap())),
61			snapshot_index: LogIndex::new(u64::from_le_bytes(buf[40..48].try_into().unwrap())),
62			snapshot_term: Term::new(u64::from_le_bytes(buf[48..56].try_into().unwrap())),
63		};
64		Some((state, VoteSeq::new(u64::from_le_bytes(buf[8..16].try_into().unwrap()))))
65	}
66}
67
68fn voted_for_bits(voted_for: Option<NodeId>) -> u64 {
69	match voted_for {
70		Some(node) => node.as_u64(),
71		None => NONE,
72	}
73}
74
75fn voted_for_of(bits: u64) -> Option<NodeId> {
76	if bits == NONE {
77		None
78	} else {
79		Some(NodeId::new(bits))
80	}
81}
82
83fn checksum(bytes: &[u8]) -> u32 {
84	let mut hasher = Hasher::new();
85	hasher.update(bytes);
86	hasher.finalize()
87}
88
89#[cfg(test)]
90mod tests {
91	use super::*;
92
93	#[test]
94	fn a_slot_lays_its_fields_out_at_the_documented_offsets() {
95		// The offsets are the on disk format; moving one makes an existing vote unreadable,
96		// and an unreadable vote is a node that cannot safely rejoin.
97		let raw = State {
98			term: Term::new(0x0102030405060708),
99			voted_for: Some(NodeId::new(0x1112131415161718)),
100			commit_index: LogIndex::new(0x2122232425262728),
101			snapshot_index: LogIndex::new(0x4142434445464748),
102			snapshot_term: Term::new(0x5152535455565758),
103		}
104		.encode(VoteSeq::new(0x3132333435363738));
105
106		assert_eq!(raw.len(), SLOT_BYTES);
107		assert_eq!(&raw[0..4], &MAGIC.to_le_bytes());
108		assert_eq!(&raw[8..16], &0x3132333435363738u64.to_le_bytes());
109		assert_eq!(&raw[16..24], &0x0102030405060708u64.to_le_bytes());
110		assert_eq!(&raw[24..32], &0x1112131415161718u64.to_le_bytes());
111		assert_eq!(&raw[32..40], &0x2122232425262728u64.to_le_bytes());
112		assert_eq!(&raw[40..48], &0x4142434445464748u64.to_le_bytes());
113		assert_eq!(&raw[48..56], &0x5152535455565758u64.to_le_bytes());
114	}
115
116	#[test]
117	fn no_vote_encodes_as_the_sentinel_and_decodes_back_to_none() {
118		// Option is only sound if the sentinel survives the round trip; a node id decoded
119		// out of the none slot would let a node that never voted claim it had.
120		let raw = State::EMPTY.encode(VoteSeq::FIRST);
121
122		assert_eq!(&raw[24..32], &NONE.to_le_bytes());
123		assert_eq!(State::decode(&raw).unwrap().0.voted_for, None);
124	}
125}