chie_shared/types/
state_machine.rs

1//! State machine types with compile-time state enforcement using phantom types.
2//!
3//! This module provides zero-cost state machine abstractions using Rust's type system
4//! to prevent invalid state transitions at compile time.
5
6use serde::{Deserialize, Serialize};
7use std::marker::PhantomData;
8
9/// Bandwidth proof lifecycle states.
10pub mod proof_states {
11    /// Initial state: proof created but not submitted
12    #[derive(Debug)]
13    pub struct Created;
14    /// Proof has been submitted for verification
15    #[derive(Debug)]
16    pub struct Submitted;
17    /// Proof has been verified and accepted
18    #[derive(Debug)]
19    pub struct Verified;
20    /// Proof was rejected during verification
21    #[derive(Debug)]
22    pub struct Rejected;
23}
24
25/// Type-safe bandwidth proof with state machine enforcement.
26///
27/// Uses phantom types to ensure only valid state transitions are possible.
28///
29/// # Example
30/// ```
31/// use chie_shared::BandwidthProofState;
32/// use chie_shared::proof_states::*;
33///
34/// // Create a new proof
35/// let proof = BandwidthProofState::<Created>::new("proof123", 1024, 100);
36///
37/// // Submit for verification (changes state)
38/// let submitted = proof.submit();
39///
40/// // Verify the proof (changes state again)
41/// let verified = submitted.verify(true);
42///
43/// // Can't submit again - compile error!
44/// // let error = verified.submit(); // Error: method not available for Verified state
45/// ```
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BandwidthProofState<S> {
48    /// Proof identifier
49    pub id: String,
50    /// Bytes transferred
51    pub bytes_transferred: u64,
52    /// Latency in milliseconds
53    pub latency_ms: u64,
54    /// Points awarded (only set in Verified state)
55    pub points: u64,
56    /// Rejection reason (only set in Rejected state)
57    pub rejection_reason: Option<String>,
58    /// Phantom state marker (zero-sized)
59    #[serde(skip)]
60    _state: PhantomData<S>,
61}
62
63impl BandwidthProofState<proof_states::Created> {
64    /// Create a new bandwidth proof in Created state.
65    #[must_use]
66    pub fn new(id: impl Into<String>, bytes_transferred: u64, latency_ms: u64) -> Self {
67        Self {
68            id: id.into(),
69            bytes_transferred,
70            latency_ms,
71            points: 0,
72            rejection_reason: None,
73            _state: PhantomData,
74        }
75    }
76
77    /// Submit the proof for verification (transition to Submitted state).
78    #[must_use]
79    pub fn submit(self) -> BandwidthProofState<proof_states::Submitted> {
80        BandwidthProofState {
81            id: self.id,
82            bytes_transferred: self.bytes_transferred,
83            latency_ms: self.latency_ms,
84            points: self.points,
85            rejection_reason: self.rejection_reason,
86            _state: PhantomData,
87        }
88    }
89}
90
91impl BandwidthProofState<proof_states::Submitted> {
92    /// Verify the proof (transition to Verified or Rejected state).
93    ///
94    /// # Errors
95    ///
96    /// Returns `BandwidthProofState<Rejected>` if validation fails
97    pub fn verify(
98        self,
99        valid: bool,
100    ) -> Result<
101        BandwidthProofState<proof_states::Verified>,
102        BandwidthProofState<proof_states::Rejected>,
103    > {
104        if valid {
105            // Calculate points (simplified)
106            let points = self.bytes_transferred / 1_000_000;
107            Ok(BandwidthProofState {
108                id: self.id,
109                bytes_transferred: self.bytes_transferred,
110                latency_ms: self.latency_ms,
111                points,
112                rejection_reason: None,
113                _state: PhantomData,
114            })
115        } else {
116            Err(BandwidthProofState {
117                id: self.id,
118                bytes_transferred: self.bytes_transferred,
119                latency_ms: self.latency_ms,
120                points: 0,
121                rejection_reason: Some("Verification failed".to_string()),
122                _state: PhantomData,
123            })
124        }
125    }
126}
127
128impl BandwidthProofState<proof_states::Verified> {
129    /// Get the awarded points (only available in Verified state).
130    #[must_use]
131    pub fn awarded_points(&self) -> u64 {
132        self.points
133    }
134}
135
136impl BandwidthProofState<proof_states::Rejected> {
137    /// Get the rejection reason (only available in Rejected state).
138    #[must_use]
139    pub fn reason(&self) -> &str {
140        self.rejection_reason
141            .as_deref()
142            .unwrap_or("Unknown rejection reason")
143    }
144}
145
146/// Content upload lifecycle states.
147pub mod content_states {
148    /// Content is being uploaded
149    #[derive(Debug)]
150    pub struct Uploading;
151    /// Upload complete, pending processing
152    #[derive(Debug)]
153    pub struct Processing;
154    /// Content is published and available
155    #[derive(Debug)]
156    pub struct Published;
157    /// Content is archived
158    #[derive(Debug)]
159    pub struct Archived;
160}
161
162/// Type-safe content upload with state machine enforcement.
163#[derive(Debug, Clone)]
164pub struct ContentUpload<S> {
165    /// Content identifier
166    pub content_id: String,
167    /// Upload progress (bytes)
168    pub uploaded_bytes: u64,
169    /// Total size (bytes)
170    pub total_bytes: u64,
171    /// CID (only set in Published state)
172    pub cid: Option<String>,
173    _state: PhantomData<S>,
174}
175
176impl ContentUpload<content_states::Uploading> {
177    /// Create a new content upload.
178    #[must_use]
179    pub fn new(content_id: impl Into<String>, total_bytes: u64) -> Self {
180        Self {
181            content_id: content_id.into(),
182            uploaded_bytes: 0,
183            total_bytes,
184            cid: None,
185            _state: PhantomData,
186        }
187    }
188
189    /// Update upload progress.
190    pub fn update_progress(&mut self, bytes: u64) {
191        self.uploaded_bytes = self
192            .uploaded_bytes
193            .saturating_add(bytes)
194            .min(self.total_bytes);
195    }
196
197    /// Check if upload is complete.
198    #[must_use]
199    pub fn is_complete(&self) -> bool {
200        self.uploaded_bytes >= self.total_bytes
201    }
202
203    /// Transition to Processing state when upload completes.
204    #[must_use]
205    pub fn complete_upload(self) -> ContentUpload<content_states::Processing> {
206        ContentUpload {
207            content_id: self.content_id,
208            uploaded_bytes: self.uploaded_bytes,
209            total_bytes: self.total_bytes,
210            cid: None,
211            _state: PhantomData,
212        }
213    }
214}
215
216impl ContentUpload<content_states::Processing> {
217    /// Finish processing and publish content.
218    #[must_use]
219    pub fn publish(self, cid: impl Into<String>) -> ContentUpload<content_states::Published> {
220        ContentUpload {
221            content_id: self.content_id,
222            uploaded_bytes: self.uploaded_bytes,
223            total_bytes: self.total_bytes,
224            cid: Some(cid.into()),
225            _state: PhantomData,
226        }
227    }
228}
229
230impl ContentUpload<content_states::Published> {
231    /// Get the content CID (only available in Published state).
232    #[must_use]
233    pub fn cid(&self) -> &str {
234        self.cid.as_deref().unwrap_or("")
235    }
236
237    /// Archive the content.
238    #[must_use]
239    pub fn archive(self) -> ContentUpload<content_states::Archived> {
240        ContentUpload {
241            content_id: self.content_id,
242            uploaded_bytes: self.uploaded_bytes,
243            total_bytes: self.total_bytes,
244            cid: self.cid,
245            _state: PhantomData,
246        }
247    }
248}
249
250impl ContentUpload<content_states::Archived> {
251    /// Get the archived content CID.
252    #[must_use]
253    pub fn archived_cid(&self) -> &str {
254        self.cid.as_deref().unwrap_or("")
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_proof_state_machine_happy_path() {
264        // Create proof
265        let proof = BandwidthProofState::<proof_states::Created>::new("proof1", 10_000_000, 100);
266        assert_eq!(proof.id, "proof1");
267        assert_eq!(proof.bytes_transferred, 10_000_000);
268
269        // Submit proof
270        let submitted = proof.submit();
271        assert_eq!(submitted.id, "proof1");
272
273        // Verify proof
274        let verified = submitted.verify(true).unwrap();
275        assert_eq!(verified.id, "proof1");
276        assert_eq!(verified.awarded_points(), 10); // 10_000_000 / 1_000_000
277    }
278
279    #[test]
280    fn test_proof_state_machine_rejection() {
281        let proof = BandwidthProofState::<proof_states::Created>::new("proof1", 1024, 100);
282        let submitted = proof.submit();
283        let rejected = submitted.verify(false).unwrap_err();
284
285        assert_eq!(rejected.id, "proof1");
286        assert!(rejected.reason().contains("Verification failed"));
287    }
288
289    #[test]
290    fn test_content_upload_state_machine() {
291        // Start upload
292        let mut upload = ContentUpload::<content_states::Uploading>::new("content1", 1000);
293        assert_eq!(upload.uploaded_bytes, 0);
294        assert!(!upload.is_complete());
295
296        // Update progress
297        upload.update_progress(500);
298        assert_eq!(upload.uploaded_bytes, 500);
299        assert!(!upload.is_complete());
300
301        // Complete upload
302        upload.update_progress(500);
303        assert!(upload.is_complete());
304
305        let processing = upload.complete_upload();
306        assert_eq!(processing.content_id, "content1");
307
308        // Publish
309        let published = processing.publish("QmXXX123");
310        assert_eq!(published.cid(), "QmXXX123");
311
312        // Archive
313        let archived = published.archive();
314        assert_eq!(archived.archived_cid(), "QmXXX123");
315    }
316
317    #[test]
318    fn test_content_upload_progress_clamping() {
319        let mut upload = ContentUpload::<content_states::Uploading>::new("content1", 100);
320        upload.update_progress(150); // More than total
321        assert_eq!(upload.uploaded_bytes, 100); // Clamped to total
322    }
323
324    #[test]
325    fn test_proof_serde() {
326        let proof = BandwidthProofState::<proof_states::Created>::new("proof1", 1024, 50);
327        let json = serde_json::to_string(&proof).unwrap();
328        let _decoded: BandwidthProofState<proof_states::Created> =
329            serde_json::from_str(&json).unwrap();
330        // PhantomData doesn't serialize, but struct does
331    }
332
333    // Compile-time tests (these should fail to compile if uncommented)
334    // #[test]
335    // fn test_invalid_transitions() {
336    //     let proof = BandwidthProofState::<proof_states::Created>::new("proof1", 1024, 100);
337    //     let verified = proof.verify(true); // Error: verify() only available on Submitted
338    //
339    //     let submitted = proof.submit();
340    //     let resubmitted = submitted.submit(); // Error: submit() only available on Created
341    // }
342}