Skip to main content

nklave_core/state/
integrity.rs

1//! State integrity management with hash chaining
2//!
3//! Provides rollback detection through cryptographic chaining of decisions
4
5use crate::policy::types::{PolicyDecision, SigningType};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use sha2::{Digest, Sha256};
8
9/// Serialize a fixed-size byte array as hex
10fn 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
17/// Deserialize a fixed-size byte array from hex
18fn 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
37/// Serialize Option<[u8; N]> as hex
38fn 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
51/// Deserialize Option<[u8; N]> from hex
52fn 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/// State integrity tracker
79///
80/// Maintains a hash chain of all signing decisions to detect rollback attacks
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct StateIntegrity {
83    /// Current state hash (hash of all decisions up to now)
84    #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
85    pub current_hash: [u8; 32],
86
87    /// Sequence number of the last recorded decision
88    pub sequence_number: u64,
89
90    /// Genesis validators root (locked on first signing request)
91    #[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    /// Create a new state integrity tracker
97    pub fn new() -> Self {
98        Self {
99            current_hash: [0u8; 32], // Initial hash is all zeros
100            sequence_number: 0,
101            genesis_validators_root: None,
102        }
103    }
104
105    /// Create from a checkpoint
106    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    /// Lock the genesis validators root (can only be set once)
115    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(()), // Already set to same value
122            None => {
123                self.genesis_validators_root = Some(root);
124                Ok(())
125            }
126        }
127    }
128
129    /// Record a new decision and update the hash chain
130    ///
131    /// Returns the new state hash after recording
132    pub fn record_decision(&mut self, record: &DecisionRecord) -> Result<[u8; 32], IntegrityError> {
133        // Verify sequence continuity
134        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        // Verify hash chain continuity
143        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        // Compute new hash: H(prev_hash || record_bytes)
151        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        // Update state
159        self.current_hash = new_hash;
160        self.sequence_number = record.sequence;
161
162        Ok(new_hash)
163    }
164
165    /// Create a decision record with proper sequencing
166    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    /// Create a decision record with proper sequencing and signing context
189    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    /// Verify a sequence of records against expected hash chain
213    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            // Compute expected new hash
238            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/// Context for signing request - captures slot/epoch data for state recovery
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub enum SigningContext {
260    // Ethereum contexts
261    /// Block proposal context (Ethereum)
262    BlockProposal {
263        /// Slot number for the block
264        slot: u64,
265    },
266    /// Attestation context (Ethereum)
267    Attestation {
268        /// Source epoch
269        source_epoch: u64,
270        /// Target epoch
271        target_epoch: u64,
272    },
273
274    // Cosmos contexts
275    /// Cosmos vote context (prevote or precommit)
276    CosmosVote {
277        /// Block height
278        height: i64,
279        /// Consensus round
280        round: i32,
281        /// Vote type (0x01 = prevote, 0x02 = precommit)
282        vote_type: u8,
283        /// Block hash being voted for (None = nil vote)
284        block_hash: Option<[u8; 32]>,
285    },
286    /// Cosmos proposal context
287    CosmosProposal {
288        /// Block height
289        height: i64,
290        /// Consensus round
291        round: i32,
292        /// Block hash being proposed
293        block_hash: [u8; 32],
294    },
295
296    /// Other signing operations (RANDAO, sync committee, etc.)
297    Other,
298}
299
300/// A record of a signing decision
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DecisionRecord {
303    /// Sequence number (monotonically increasing)
304    pub sequence: u64,
305
306    /// Unix timestamp of the decision
307    pub timestamp: u64,
308
309    /// Validator public key
310    #[serde(serialize_with = "serialize_bytes::<_, 48>", deserialize_with = "deserialize_bytes::<_, 48>")]
311    pub validator_pubkey: [u8; 48],
312
313    /// Type of signing request
314    pub request_type: SigningType,
315
316    /// Decision made (Allow or Refuse with code)
317    pub decision: PolicyDecision,
318
319    /// Signing root of the request
320    #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
321    pub signing_root: [u8; 32],
322
323    /// Hash of state before this decision
324    #[serde(serialize_with = "serialize_bytes::<_, 32>", deserialize_with = "deserialize_bytes::<_, 32>")]
325    pub prev_state_hash: [u8; 32],
326
327    /// Signing context with slot/epoch data for state recovery
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub signing_context: Option<SigningContext>,
330}
331
332impl DecisionRecord {
333    /// Compute the hash of this record
334    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/// Errors related to state integrity
343#[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        // Create a record with wrong sequence
405        let mut record = integrity.prepare_record(
406            [0u8; 48],
407            SigningType::BlockProposal,
408            PolicyDecision::Allow,
409            make_root(1),
410        );
411        record.sequence = 5; // Wrong sequence
412
413        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        // Create a record with wrong prev_state_hash
422        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); // Wrong hash
429
430        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        // First set should succeed
441        assert!(integrity.set_genesis_validators_root(root1).is_ok());
442
443        // Same root should succeed
444        assert!(integrity.set_genesis_validators_root(root1).is_ok());
445
446        // Different root should fail
447        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        // Create and record multiple decisions
459        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        // Verify from the beginning
471        let fresh_integrity = StateIntegrity::new();
472        assert!(fresh_integrity.verify_records(&records).is_ok());
473    }
474}