1use crate::policy::types::{PolicyDecision, SigningType};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use sha2::{Digest, Sha256};
8
9fn serialize_bytes<S, const N: usize>(bytes: &[u8; N], serializer: S) -> Result<S::Ok, S::Error>
11where
12 S: Serializer,
13{
14 serializer.serialize_str(&hex::encode(bytes))
15}
16
17fn deserialize_bytes<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
19where
20 D: Deserializer<'de>,
21{
22 let s: String = Deserialize::deserialize(deserializer)?;
23 let s = s.strip_prefix("0x").unwrap_or(&s);
24 let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
25 if bytes.len() != N {
26 return Err(serde::de::Error::custom(format!(
27 "expected {} bytes, got {}",
28 N,
29 bytes.len()
30 )));
31 }
32 let mut arr = [0u8; N];
33 arr.copy_from_slice(&bytes);
34 Ok(arr)
35}
36
37fn serialize_option_bytes<S, const N: usize>(
39 bytes: &Option<[u8; N]>,
40 serializer: S,
41) -> Result<S::Ok, S::Error>
42where
43 S: Serializer,
44{
45 match bytes {
46 Some(b) => serializer.serialize_some(&hex::encode(b)),
47 None => serializer.serialize_none(),
48 }
49}
50
51fn deserialize_option_bytes<'de, D, const N: usize>(
53 deserializer: D,
54) -> Result<Option<[u8; N]>, D::Error>
55where
56 D: Deserializer<'de>,
57{
58 let opt: Option<String> = Deserialize::deserialize(deserializer)?;
59 match opt {
60 Some(s) => {
61 let s = s.strip_prefix("0x").unwrap_or(&s);
62 let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
63 if bytes.len() != N {
64 return Err(serde::de::Error::custom(format!(
65 "expected {} bytes, got {}",
66 N,
67 bytes.len()
68 )));
69 }
70 let mut arr = [0u8; N];
71 arr.copy_from_slice(&bytes);
72 Ok(Some(arr))
73 }
74 None => Ok(None),
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct StateIntegrity {
83 #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
85 pub current_hash: [u8; 32],
86
87 pub sequence_number: u64,
89
90 #[serde(serialize_with = "serialize_option_bytes::<_, 32>", deserialize_with = "deserialize_option_bytes::<_, 32>")]
92 pub genesis_validators_root: Option<[u8; 32]>,
93}
94
95impl StateIntegrity {
96 pub fn new() -> Self {
98 Self {
99 current_hash: [0u8; 32], sequence_number: 0,
101 genesis_validators_root: None,
102 }
103 }
104
105 pub fn from_checkpoint(hash: [u8; 32], sequence: u64, genesis_root: Option<[u8; 32]>) -> Self {
107 Self {
108 current_hash: hash,
109 sequence_number: sequence,
110 genesis_validators_root: genesis_root,
111 }
112 }
113
114 pub fn set_genesis_validators_root(&mut self, root: [u8; 32]) -> Result<(), IntegrityError> {
116 match self.genesis_validators_root {
117 Some(existing) if existing != root => Err(IntegrityError::GenesisRootMismatch {
118 expected: existing,
119 actual: root,
120 }),
121 Some(_) => Ok(()), None => {
123 self.genesis_validators_root = Some(root);
124 Ok(())
125 }
126 }
127 }
128
129 pub fn record_decision(&mut self, record: &DecisionRecord) -> Result<[u8; 32], IntegrityError> {
133 let expected_sequence = self.sequence_number + 1;
135 if record.sequence != expected_sequence {
136 return Err(IntegrityError::SequenceGap {
137 expected: expected_sequence,
138 actual: record.sequence,
139 });
140 }
141
142 if record.prev_state_hash != self.current_hash {
144 return Err(IntegrityError::HashMismatch {
145 expected: self.current_hash,
146 actual: record.prev_state_hash,
147 });
148 }
149
150 let record_bytes = bincode::serialize(record).map_err(|e| IntegrityError::SerializationError(e.to_string()))?;
152
153 let mut hasher = Sha256::new();
154 hasher.update(self.current_hash);
155 hasher.update(&record_bytes);
156 let new_hash: [u8; 32] = hasher.finalize().into();
157
158 self.current_hash = new_hash;
160 self.sequence_number = record.sequence;
161
162 Ok(new_hash)
163 }
164
165 pub fn prepare_record(
167 &self,
168 validator_pubkey: [u8; 48],
169 request_type: SigningType,
170 decision: PolicyDecision,
171 signing_root: [u8; 32],
172 ) -> DecisionRecord {
173 DecisionRecord {
174 sequence: self.sequence_number + 1,
175 timestamp: std::time::SystemTime::now()
176 .duration_since(std::time::UNIX_EPOCH)
177 .unwrap_or_default()
178 .as_secs(),
179 validator_pubkey,
180 request_type,
181 decision,
182 signing_root,
183 prev_state_hash: self.current_hash,
184 signing_context: None,
185 }
186 }
187
188 pub fn prepare_record_with_context(
190 &self,
191 validator_pubkey: [u8; 48],
192 request_type: SigningType,
193 decision: PolicyDecision,
194 signing_root: [u8; 32],
195 signing_context: SigningContext,
196 ) -> DecisionRecord {
197 DecisionRecord {
198 sequence: self.sequence_number + 1,
199 timestamp: std::time::SystemTime::now()
200 .duration_since(std::time::UNIX_EPOCH)
201 .unwrap_or_default()
202 .as_secs(),
203 validator_pubkey,
204 request_type,
205 decision,
206 signing_root,
207 prev_state_hash: self.current_hash,
208 signing_context: Some(signing_context),
209 }
210 }
211
212 pub fn verify_records<'a, I>(&self, records: I) -> Result<(), IntegrityError>
214 where
215 I: IntoIterator<Item = &'a DecisionRecord>,
216 {
217 let mut expected_hash = self.current_hash;
218 let mut expected_sequence = self.sequence_number;
219
220 for record in records {
221 expected_sequence += 1;
222
223 if record.sequence != expected_sequence {
224 return Err(IntegrityError::SequenceGap {
225 expected: expected_sequence,
226 actual: record.sequence,
227 });
228 }
229
230 if record.prev_state_hash != expected_hash {
231 return Err(IntegrityError::HashMismatch {
232 expected: expected_hash,
233 actual: record.prev_state_hash,
234 });
235 }
236
237 let record_bytes = bincode::serialize(record)
239 .map_err(|e| IntegrityError::SerializationError(e.to_string()))?;
240
241 let mut hasher = Sha256::new();
242 hasher.update(expected_hash);
243 hasher.update(&record_bytes);
244 expected_hash = hasher.finalize().into();
245 }
246
247 Ok(())
248 }
249}
250
251impl Default for StateIntegrity {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub enum SigningContext {
260 BlockProposal {
263 slot: u64,
265 },
266 Attestation {
268 source_epoch: u64,
270 target_epoch: u64,
272 },
273
274 CosmosVote {
277 height: i64,
279 round: i32,
281 vote_type: u8,
283 block_hash: Option<[u8; 32]>,
285 },
286 CosmosProposal {
288 height: i64,
290 round: i32,
292 block_hash: [u8; 32],
294 },
295
296 Other,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DecisionRecord {
303 pub sequence: u64,
305
306 pub timestamp: u64,
308
309 #[serde(serialize_with = "serialize_bytes::<_, 48>", deserialize_with = "deserialize_bytes::<_, 48>")]
311 pub validator_pubkey: [u8; 48],
312
313 pub request_type: SigningType,
315
316 pub decision: PolicyDecision,
318
319 #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
321 pub signing_root: [u8; 32],
322
323 #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
325 pub prev_state_hash: [u8; 32],
326
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub signing_context: Option<SigningContext>,
330}
331
332impl DecisionRecord {
333 pub fn hash(&self) -> [u8; 32] {
335 let bytes = bincode::serialize(self).expect("serialization should not fail");
336 let mut hasher = Sha256::new();
337 hasher.update(&bytes);
338 hasher.finalize().into()
339 }
340}
341
342#[derive(Debug, Clone, thiserror::Error)]
344pub enum IntegrityError {
345 #[error("Sequence gap: expected {expected}, got {actual}")]
346 SequenceGap { expected: u64, actual: u64 },
347
348 #[error("Hash mismatch: expected {expected:?}, got {actual:?}")]
349 HashMismatch { expected: [u8; 32], actual: [u8; 32] },
350
351 #[error("Genesis validators root mismatch: expected {expected:?}, got {actual:?}")]
352 GenesisRootMismatch { expected: [u8; 32], actual: [u8; 32] },
353
354 #[error("Serialization error: {0}")]
355 SerializationError(String),
356
357 #[error("Log truncated: missing records after sequence {last_seen}")]
358 LogTruncated { last_seen: u64 },
359
360 #[error("Log corrupted: invalid record at sequence {sequence}")]
361 LogCorrupted { sequence: u64 },
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 fn make_root(val: u8) -> [u8; 32] {
369 let mut root = [0u8; 32];
370 root[0] = val;
371 root
372 }
373
374 #[test]
375 fn test_state_integrity_new() {
376 let integrity = StateIntegrity::new();
377 assert_eq!(integrity.current_hash, [0u8; 32]);
378 assert_eq!(integrity.sequence_number, 0);
379 assert!(integrity.genesis_validators_root.is_none());
380 }
381
382 #[test]
383 fn test_record_decision() {
384 let mut integrity = StateIntegrity::new();
385
386 let record = integrity.prepare_record(
387 [0u8; 48],
388 SigningType::BlockProposal,
389 PolicyDecision::Allow,
390 make_root(1),
391 );
392
393 let new_hash = integrity.record_decision(&record).unwrap();
394
395 assert_ne!(new_hash, [0u8; 32]);
396 assert_eq!(integrity.sequence_number, 1);
397 assert_eq!(integrity.current_hash, new_hash);
398 }
399
400 #[test]
401 fn test_sequence_gap_detection() {
402 let mut integrity = StateIntegrity::new();
403
404 let mut record = integrity.prepare_record(
406 [0u8; 48],
407 SigningType::BlockProposal,
408 PolicyDecision::Allow,
409 make_root(1),
410 );
411 record.sequence = 5; let result = integrity.record_decision(&record);
414 assert!(matches!(result, Err(IntegrityError::SequenceGap { .. })));
415 }
416
417 #[test]
418 fn test_hash_mismatch_detection() {
419 let mut integrity = StateIntegrity::new();
420
421 let mut record = integrity.prepare_record(
423 [0u8; 48],
424 SigningType::BlockProposal,
425 PolicyDecision::Allow,
426 make_root(1),
427 );
428 record.prev_state_hash = make_root(99); let result = integrity.record_decision(&record);
431 assert!(matches!(result, Err(IntegrityError::HashMismatch { .. })));
432 }
433
434 #[test]
435 fn test_genesis_root_locking() {
436 let mut integrity = StateIntegrity::new();
437 let root1 = make_root(1);
438 let root2 = make_root(2);
439
440 assert!(integrity.set_genesis_validators_root(root1).is_ok());
442
443 assert!(integrity.set_genesis_validators_root(root1).is_ok());
445
446 assert!(matches!(
448 integrity.set_genesis_validators_root(root2),
449 Err(IntegrityError::GenesisRootMismatch { .. })
450 ));
451 }
452
453 #[test]
454 fn test_verify_records() {
455 let mut integrity = StateIntegrity::new();
456 let mut records = Vec::new();
457
458 for i in 0..3 {
460 let record = integrity.prepare_record(
461 [0u8; 48],
462 SigningType::BlockProposal,
463 PolicyDecision::Allow,
464 make_root(i),
465 );
466 integrity.record_decision(&record).unwrap();
467 records.push(record);
468 }
469
470 let fresh_integrity = StateIntegrity::new();
472 assert!(fresh_integrity.verify_records(&records).is_ok());
473 }
474}