chie_shared/types/
batch.rs

1//! Batch operation types for efficient processing in CHIE Protocol.
2//!
3//! This module provides types for batching operations like proof submissions,
4//! content announcements, and statistics updates.
5
6#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use super::{BandwidthProof, ContentCid, PeerIdString, Points};
11
12/// Batch submission of bandwidth proofs.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(JsonSchema))]
15pub struct BatchProofSubmission {
16    /// List of proofs to submit.
17    pub proofs: Vec<BandwidthProof>,
18    /// Batch ID for tracking.
19    pub batch_id: uuid::Uuid,
20    /// Submitter peer ID.
21    pub peer_id: PeerIdString,
22    /// Submission timestamp (Unix milliseconds).
23    pub timestamp_ms: i64,
24}
25
26impl BatchProofSubmission {
27    /// Create a new batch proof submission.
28    ///
29    /// # Example
30    ///
31    /// ```
32    /// use chie_shared::types::batch::BatchProofSubmission;
33    /// use chie_shared::types::bandwidth::BandwidthProofBuilder;
34    ///
35    /// // Create multiple bandwidth proofs
36    /// let proof1 = BandwidthProofBuilder::new()
37    ///     .content_cid("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
38    ///     .provider_peer_id("12D3KooWProvider")
39    ///     .requester_peer_id("12D3KooWRequester")
40    ///     .provider_public_key(vec![1u8; 32])
41    ///     .requester_public_key(vec![2u8; 32])
42    ///     .provider_signature(vec![3u8; 64])
43    ///     .requester_signature(vec![4u8; 64])
44    ///     .challenge_nonce(vec![5u8; 32])
45    ///     .chunk_hash(vec![6u8; 32])
46    ///     .bytes_transferred(262_144)
47    ///     .timestamps(1000, 1100)
48    ///     .build()
49    ///     .unwrap();
50    ///
51    /// let proof2 = BandwidthProofBuilder::new()
52    ///     .content_cid("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
53    ///     .chunk_index(1)
54    ///     .provider_peer_id("12D3KooWProvider")
55    ///     .requester_peer_id("12D3KooWRequester")
56    ///     .provider_public_key(vec![1u8; 32])
57    ///     .requester_public_key(vec![2u8; 32])
58    ///     .provider_signature(vec![3u8; 64])
59    ///     .requester_signature(vec![4u8; 64])
60    ///     .challenge_nonce(vec![5u8; 32])
61    ///     .chunk_hash(vec![6u8; 32])
62    ///     .bytes_transferred(262_144)
63    ///     .timestamps(1100, 1200)
64    ///     .build()
65    ///     .unwrap();
66    ///
67    /// // Submit proofs as a batch
68    /// let batch = BatchProofSubmission::new(
69    ///     vec![proof1, proof2],
70    ///     "12D3KooWProvider"
71    /// );
72    ///
73    /// assert_eq!(batch.proof_count(), 2);
74    /// assert_eq!(batch.total_bytes_transferred(), 524_288);
75    /// assert!(!batch.is_empty());
76    /// ```
77    #[must_use]
78    pub fn new(proofs: Vec<BandwidthProof>, peer_id: impl Into<String>) -> Self {
79        Self {
80            proofs,
81            batch_id: uuid::Uuid::new_v4(),
82            peer_id: peer_id.into(),
83            timestamp_ms: crate::now_ms(),
84        }
85    }
86
87    /// Get the number of proofs in the batch.
88    #[must_use]
89    pub fn proof_count(&self) -> usize {
90        self.proofs.len()
91    }
92
93    /// Check if batch is empty.
94    #[must_use]
95    pub fn is_empty(&self) -> bool {
96        self.proofs.is_empty()
97    }
98
99    /// Calculate total bytes transferred across all proofs.
100    #[must_use]
101    pub fn total_bytes_transferred(&self) -> u64 {
102        self.proofs.iter().map(|p| p.bytes_transferred).sum()
103    }
104}
105
106/// Response to batch proof submission.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[cfg_attr(feature = "schema", derive(JsonSchema))]
109pub struct BatchProofResponse {
110    /// Batch ID that was processed.
111    pub batch_id: uuid::Uuid,
112    /// Number of proofs accepted.
113    pub accepted_count: usize,
114    /// Number of proofs rejected.
115    pub rejected_count: usize,
116    /// Total reward points for accepted proofs.
117    pub total_reward_points: Points,
118    /// Individual results per proof.
119    pub results: Vec<ProofResult>,
120}
121
122impl BatchProofResponse {
123    /// Create a new batch response.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use chie_shared::types::batch::{BatchProofResponse, ProofResult};
129    /// use uuid::Uuid;
130    ///
131    /// let batch_id = Uuid::new_v4();
132    /// let mut response = BatchProofResponse::new(batch_id);
133    ///
134    /// // Add results
135    /// response.results.push(ProofResult::accepted(0, Uuid::new_v4(), 1000));
136    /// response.results.push(ProofResult::accepted(1, Uuid::new_v4(), 1500));
137    /// response.results.push(ProofResult::rejected(2, "Invalid signature"));
138    ///
139    /// response.accepted_count = 2;
140    /// response.rejected_count = 1;
141    /// response.total_reward_points = 2500;
142    ///
143    /// assert_eq!(response.total_count(), 3);
144    /// assert_eq!(response.acceptance_rate(), 2.0 / 3.0);
145    /// assert!(!response.all_accepted());
146    /// assert!(!response.all_rejected());
147    /// ```
148    #[must_use]
149    pub fn new(batch_id: uuid::Uuid) -> Self {
150        Self {
151            batch_id,
152            accepted_count: 0,
153            rejected_count: 0,
154            total_reward_points: 0,
155            results: Vec::new(),
156        }
157    }
158
159    /// Get total number of proofs processed.
160    #[must_use]
161    pub fn total_count(&self) -> usize {
162        self.accepted_count + self.rejected_count
163    }
164
165    /// Get acceptance rate (0.0 to 1.0).
166    #[must_use]
167    #[allow(clippy::cast_precision_loss)]
168    pub fn acceptance_rate(&self) -> f64 {
169        let total = self.total_count();
170        if total == 0 {
171            0.0
172        } else {
173            self.accepted_count as f64 / total as f64
174        }
175    }
176
177    /// Check if all proofs were accepted.
178    #[must_use]
179    pub fn all_accepted(&self) -> bool {
180        self.rejected_count == 0 && self.accepted_count > 0
181    }
182
183    /// Check if all proofs were rejected.
184    #[must_use]
185    pub fn all_rejected(&self) -> bool {
186        self.accepted_count == 0 && self.rejected_count > 0
187    }
188}
189
190/// Result for an individual proof in a batch.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[cfg_attr(feature = "schema", derive(JsonSchema))]
193pub struct ProofResult {
194    /// Index in the original batch.
195    pub index: usize,
196    /// Whether the proof was accepted.
197    pub accepted: bool,
198    /// Proof ID if accepted.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub proof_id: Option<uuid::Uuid>,
201    /// Reward points if accepted.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub reward_points: Option<Points>,
204    /// Rejection reason if not accepted.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub rejection_reason: Option<String>,
207}
208
209impl ProofResult {
210    /// Create a result for an accepted proof.
211    ///
212    /// # Example
213    ///
214    /// ```
215    /// use chie_shared::types::batch::ProofResult;
216    /// use uuid::Uuid;
217    ///
218    /// let proof_id = Uuid::new_v4();
219    /// let result = ProofResult::accepted(0, proof_id, 1000);
220    ///
221    /// assert!(result.accepted);
222    /// assert_eq!(result.index, 0);
223    /// assert_eq!(result.proof_id, Some(proof_id));
224    /// assert_eq!(result.reward_points, Some(1000));
225    /// assert_eq!(result.rejection_reason, None);
226    /// ```
227    #[must_use]
228    pub fn accepted(index: usize, proof_id: uuid::Uuid, reward_points: Points) -> Self {
229        Self {
230            index,
231            accepted: true,
232            proof_id: Some(proof_id),
233            reward_points: Some(reward_points),
234            rejection_reason: None,
235        }
236    }
237
238    /// Create a result for a rejected proof.
239    ///
240    /// # Example
241    ///
242    /// ```
243    /// use chie_shared::types::batch::ProofResult;
244    ///
245    /// let result = ProofResult::rejected(2, "Invalid signature length");
246    ///
247    /// assert!(!result.accepted);
248    /// assert_eq!(result.index, 2);
249    /// assert_eq!(result.proof_id, None);
250    /// assert_eq!(result.reward_points, None);
251    /// assert_eq!(result.rejection_reason, Some("Invalid signature length".to_string()));
252    /// ```
253    #[must_use]
254    pub fn rejected(index: usize, reason: impl Into<String>) -> Self {
255        Self {
256            index,
257            accepted: false,
258            proof_id: None,
259            reward_points: None,
260            rejection_reason: Some(reason.into()),
261        }
262    }
263}
264
265/// Batch content announcement.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[cfg_attr(feature = "schema", derive(JsonSchema))]
268pub struct BatchContentAnnouncement {
269    /// List of content CIDs being announced.
270    pub content_cids: Vec<ContentCid>,
271    /// Peer ID of the announcing node.
272    pub peer_id: PeerIdString,
273    /// Batch ID for tracking.
274    pub batch_id: uuid::Uuid,
275    /// Announcement timestamp (Unix milliseconds).
276    pub timestamp_ms: i64,
277}
278
279impl BatchContentAnnouncement {
280    /// Create a new batch content announcement.
281    #[must_use]
282    pub fn new(content_cids: Vec<ContentCid>, peer_id: impl Into<String>) -> Self {
283        Self {
284            content_cids,
285            peer_id: peer_id.into(),
286            batch_id: uuid::Uuid::new_v4(),
287            timestamp_ms: crate::now_ms(),
288        }
289    }
290
291    /// Get the number of content items announced.
292    #[must_use]
293    pub fn content_count(&self) -> usize {
294        self.content_cids.len()
295    }
296
297    /// Check if batch is empty.
298    #[must_use]
299    pub fn is_empty(&self) -> bool {
300        self.content_cids.is_empty()
301    }
302}
303
304/// Batch statistics update.
305#[derive(Debug, Clone, Serialize, Deserialize)]
306#[cfg_attr(feature = "schema", derive(JsonSchema))]
307pub struct BatchStatsUpdate {
308    /// List of stat updates.
309    pub updates: Vec<StatUpdate>,
310    /// Batch ID for tracking.
311    pub batch_id: uuid::Uuid,
312    /// Update timestamp (Unix milliseconds).
313    pub timestamp_ms: i64,
314}
315
316impl BatchStatsUpdate {
317    /// Create a new batch stats update.
318    #[must_use]
319    pub fn new(updates: Vec<StatUpdate>) -> Self {
320        Self {
321            updates,
322            batch_id: uuid::Uuid::new_v4(),
323            timestamp_ms: crate::now_ms(),
324        }
325    }
326
327    /// Get the number of updates in the batch.
328    #[must_use]
329    pub fn update_count(&self) -> usize {
330        self.updates.len()
331    }
332
333    /// Check if batch is empty.
334    #[must_use]
335    pub fn is_empty(&self) -> bool {
336        self.updates.is_empty()
337    }
338}
339
340/// Individual statistics update.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342#[cfg_attr(feature = "schema", derive(JsonSchema))]
343pub struct StatUpdate {
344    /// Metric name.
345    pub metric: String,
346    /// Metric value.
347    pub value: f64,
348    /// Associated entity (e.g., peer ID, content CID).
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub entity: Option<String>,
351}
352
353impl StatUpdate {
354    /// Create a new stat update.
355    #[must_use]
356    pub fn new(metric: impl Into<String>, value: f64) -> Self {
357        Self {
358            metric: metric.into(),
359            value,
360            entity: None,
361        }
362    }
363
364    /// Create a stat update with entity.
365    #[must_use]
366    pub fn with_entity(metric: impl Into<String>, value: f64, entity: impl Into<String>) -> Self {
367        Self {
368            metric: metric.into(),
369            value,
370            entity: Some(entity.into()),
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_batch_proof_submission() {
381        let proof1 = crate::test_helpers::create_test_proof();
382        let proof2 = crate::test_helpers::create_test_proof();
383
384        let batch = BatchProofSubmission::new(vec![proof1, proof2], "12D3KooTest");
385
386        assert_eq!(batch.proof_count(), 2);
387        assert!(!batch.is_empty());
388        assert!(batch.total_bytes_transferred() > 0);
389    }
390
391    #[test]
392    fn test_batch_proof_response() {
393        let batch_id = uuid::Uuid::new_v4();
394        let mut response = BatchProofResponse::new(batch_id);
395
396        response.accepted_count = 8;
397        response.rejected_count = 2;
398        response.total_reward_points = 1000;
399
400        assert_eq!(response.total_count(), 10);
401        assert_eq!(response.acceptance_rate(), 0.8);
402        assert!(!response.all_accepted());
403        assert!(!response.all_rejected());
404    }
405
406    #[test]
407    fn test_batch_proof_response_all_accepted() {
408        let batch_id = uuid::Uuid::new_v4();
409        let mut response = BatchProofResponse::new(batch_id);
410
411        response.accepted_count = 10;
412        response.rejected_count = 0;
413
414        assert!(response.all_accepted());
415        assert_eq!(response.acceptance_rate(), 1.0);
416    }
417
418    #[test]
419    fn test_batch_proof_response_all_rejected() {
420        let batch_id = uuid::Uuid::new_v4();
421        let mut response = BatchProofResponse::new(batch_id);
422
423        response.accepted_count = 0;
424        response.rejected_count = 10;
425
426        assert!(response.all_rejected());
427        assert_eq!(response.acceptance_rate(), 0.0);
428    }
429
430    #[test]
431    fn test_proof_result_accepted() {
432        let proof_id = uuid::Uuid::new_v4();
433        let result = ProofResult::accepted(0, proof_id, 100);
434
435        assert!(result.accepted);
436        assert_eq!(result.proof_id, Some(proof_id));
437        assert_eq!(result.reward_points, Some(100));
438        assert!(result.rejection_reason.is_none());
439    }
440
441    #[test]
442    fn test_proof_result_rejected() {
443        let result = ProofResult::rejected(1, "Invalid signature");
444
445        assert!(!result.accepted);
446        assert!(result.proof_id.is_none());
447        assert!(result.reward_points.is_none());
448        assert_eq!(
449            result.rejection_reason,
450            Some("Invalid signature".to_string())
451        );
452    }
453
454    #[test]
455    fn test_batch_content_announcement() {
456        let cids = vec![
457            "QmTest1".to_string(),
458            "QmTest2".to_string(),
459            "QmTest3".to_string(),
460        ];
461        let batch = BatchContentAnnouncement::new(cids, "12D3KooTest");
462
463        assert_eq!(batch.content_count(), 3);
464        assert!(!batch.is_empty());
465    }
466
467    #[test]
468    fn test_batch_stats_update() {
469        let updates = vec![
470            StatUpdate::new("bandwidth_total", 1_000_000.0),
471            StatUpdate::with_entity("chunks_served", 50.0, "12D3KooTest"),
472        ];
473
474        let batch = BatchStatsUpdate::new(updates);
475
476        assert_eq!(batch.update_count(), 2);
477        assert!(!batch.is_empty());
478    }
479
480    #[test]
481    fn test_stat_update() {
482        let update1 = StatUpdate::new("test_metric", 42.0);
483        assert_eq!(update1.metric, "test_metric");
484        assert_eq!(update1.value, 42.0);
485        assert!(update1.entity.is_none());
486
487        let update2 = StatUpdate::with_entity("peer_metric", 100.0, "12D3Koo");
488        assert_eq!(update2.metric, "peer_metric");
489        assert_eq!(update2.value, 100.0);
490        assert_eq!(update2.entity, Some("12D3Koo".to_string()));
491    }
492
493    #[test]
494    fn test_batch_proof_submission_serialization() {
495        let proof = crate::test_helpers::create_test_proof();
496        let batch = BatchProofSubmission::new(vec![proof], "12D3KooTest");
497
498        let json = serde_json::to_string(&batch).unwrap();
499        let deserialized: BatchProofSubmission = serde_json::from_str(&json).unwrap();
500
501        assert_eq!(batch.batch_id, deserialized.batch_id);
502        assert_eq!(batch.proof_count(), deserialized.proof_count());
503    }
504}