Skip to main content

nectar_postage/
validation.rs

1//! Stamp validation traits and utilities.
2
3use crate::{PostageContext, Stamp, StampError};
4use nectar_primitives::SwarmAddress;
5
6#[cfg(any(test, feature = "std"))]
7use crate::Batch;
8
9#[cfg(test)]
10use crate::StampIndex;
11
12#[cfg(feature = "std")]
13use crate::{BatchStore, BatchStoreExt};
14
15/// A trait for validating postage stamps.
16///
17/// Implementations of this trait verify that stamps are valid for a given
18/// chunk address and postage context. Validation includes checking:
19///
20/// - The batch exists and is not expired
21/// - The stamp index is within valid bounds
22/// - The chunk address matches the expected bucket
23/// - The signature is valid (implementation-dependent)
24///
25/// # Example
26///
27/// ```ignore
28/// use nectar_postage::{StampValidator, Stamp, PostageContext};
29/// use nectar_primitives::SwarmAddress;
30///
31/// struct MyValidator { /* ... */ }
32///
33/// impl StampValidator for MyValidator {
34///     type Error = nectar_postage::StampError;
35///
36///     fn validate(&self, stamp: &Stamp, address: &SwarmAddress, state: &PostageContext) -> Result<(), Self::Error> {
37///         // Validation logic...
38///         Ok(())
39///     }
40/// }
41/// ```
42pub trait StampValidator {
43    /// The error type returned when validation fails.
44    type Error: From<StampError>;
45
46    /// Validates a stamp for a given chunk address.
47    ///
48    /// # Arguments
49    ///
50    /// * `stamp` - The stamp to validate
51    /// * `address` - The address of the chunk being validated
52    /// * `state` - The current postage context for expiry checks
53    ///
54    /// # Returns
55    ///
56    /// `Ok(())` if the stamp is valid, or an error describing why validation failed.
57    fn validate(
58        &self,
59        stamp: &Stamp,
60        address: &SwarmAddress,
61        state: &PostageContext,
62    ) -> Result<(), Self::Error>;
63
64    /// Validates only the structural properties of a stamp without signature verification.
65    ///
66    /// This is useful for quick validation before performing more expensive
67    /// cryptographic operations. It checks:
68    ///
69    /// - The batch exists
70    /// - The batch is usable (enough confirmations)
71    /// - The batch is not expired
72    /// - The stamp index is within valid bounds
73    /// - The chunk address matches the expected bucket
74    ///
75    /// The default implementation calls `validate`, but implementations may
76    /// override this for performance.
77    fn validate_structure(
78        &self,
79        stamp: &Stamp,
80        address: &SwarmAddress,
81        state: &PostageContext,
82    ) -> Result<(), Self::Error> {
83        self.validate(stamp, address, state)
84    }
85}
86
87// Note: BatchValidation methods (validate_index, bucket_for_address, validate_bucket)
88// are now implemented directly on the Batch type in batch.rs for better ergonomics.
89
90// Store-based Validator
91
92/// A validator that uses a [`BatchStore`] for validation.
93///
94/// This validator performs comprehensive validation:
95/// 1. Retrieves the batch from the store
96/// 2. Checks the batch is usable (enough confirmations)
97/// 3. Checks the batch is not expired
98/// 4. Validates the stamp index is within bounds
99/// 5. Validates the bucket matches the chunk address
100/// 6. Verifies the stamp signature matches the batch owner
101///
102/// # Example
103///
104/// ```ignore
105/// use nectar_postage::{StoreValidator, BatchStore};
106///
107/// let store = MyBatchStore::new();
108/// let validator = StoreValidator::new(store, 50); // 50 block confirmations
109///
110/// let result = validator.validate(&stamp, &address).await;
111/// ```
112#[derive(Debug)]
113#[cfg(feature = "std")]
114pub struct StoreValidator<S> {
115    store: S,
116    confirmation_threshold: u64,
117}
118
119#[cfg(feature = "std")]
120impl<S> StoreValidator<S> {
121    /// Creates a new store validator.
122    ///
123    /// # Arguments
124    ///
125    /// * `store` - The batch store to use for lookups
126    /// * `confirmation_threshold` - Minimum block confirmations for a batch to be usable
127    pub const fn new(store: S, confirmation_threshold: u64) -> Self {
128        Self {
129            store,
130            confirmation_threshold,
131        }
132    }
133
134    /// Returns a reference to the underlying store.
135    pub const fn store(&self) -> &S {
136        &self.store
137    }
138
139    /// Returns the confirmation threshold.
140    pub const fn confirmation_threshold(&self) -> u64 {
141        self.confirmation_threshold
142    }
143}
144
145#[cfg(feature = "std")]
146impl<S: BatchStore + Sync> StoreValidator<S> {
147    /// Validates a stamp asynchronously.
148    ///
149    /// This performs full validation including signature verification.
150    ///
151    /// # Returns
152    ///
153    /// `Ok(())` if the stamp is valid, or a [`StampError`] describing the failure.
154    pub async fn validate(&self, stamp: &Stamp, address: &SwarmAddress) -> Result<(), StampError> {
155        // Get the batch and verify it's usable
156        let batch = self.get_batch_for_stamp(stamp).await?;
157
158        // Validate structure
159        self.validate_structure_with_batch(stamp, address, &batch)?;
160
161        // Verify signature
162        stamp.verify(address, batch.owner())?;
163
164        Ok(())
165    }
166
167    /// Validates the structural properties without signature verification.
168    ///
169    /// This is faster than full validation when you only need to check
170    /// that the stamp references a valid batch and bucket.
171    pub async fn validate_structure(
172        &self,
173        stamp: &Stamp,
174        address: &SwarmAddress,
175    ) -> Result<(), StampError> {
176        let batch = self.get_batch_for_stamp(stamp).await?;
177        self.validate_structure_with_batch(stamp, address, &batch)
178    }
179
180    /// Gets and validates the batch for a stamp.
181    async fn get_batch_for_stamp(&self, stamp: &Stamp) -> Result<Batch, StampError> {
182        self.store
183            .get_usable(&stamp.batch(), self.confirmation_threshold)
184            .await
185            .map_err(|e| match e {
186                crate::BatchStoreError::NotFound(id) => StampError::BatchNotFound(id),
187                crate::BatchStoreError::NotUsable {
188                    created,
189                    current,
190                    threshold,
191                    ..
192                } => StampError::BatchNotUsable {
193                    created,
194                    current,
195                    threshold,
196                },
197                crate::BatchStoreError::Expired {
198                    value,
199                    total_amount,
200                    ..
201                } => StampError::BatchExpired {
202                    value,
203                    total_amount,
204                },
205                crate::BatchStoreError::Store(_) => StampError::BatchNotFound(stamp.batch()),
206            })
207    }
208
209    /// Validates structure given an already-retrieved batch.
210    fn validate_structure_with_batch(
211        &self,
212        stamp: &Stamp,
213        address: &SwarmAddress,
214        batch: &Batch,
215    ) -> Result<(), StampError> {
216        // Validate index bounds
217        batch.validate_index(&stamp.stamp_index())?;
218
219        // Validate bucket matches address
220        batch.validate_bucket(&stamp.stamp_index(), address)?;
221
222        Ok(())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use alloy_primitives::{Address, B256};
230
231    #[test]
232    fn test_validate_index_valid() {
233        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
234
235        // Valid: bucket < 2^16, index < 2^(18-16) = 4
236        let index = StampIndex::new(1000, 3);
237        assert!(batch.validate_index(&index).is_ok());
238    }
239
240    #[test]
241    fn test_validate_index_bucket_out_of_range() {
242        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
243
244        // Invalid: bucket >= 2^16 = 65536
245        let index = StampIndex::new(70000, 0);
246        assert!(matches!(
247            batch.validate_index(&index),
248            Err(StampError::InvalidIndex)
249        ));
250    }
251
252    #[test]
253    fn test_validate_index_position_out_of_range() {
254        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
255
256        // Invalid: index >= 2^(18-16) = 4
257        let index = StampIndex::new(1000, 5);
258        assert!(matches!(
259            batch.validate_index(&index),
260            Err(StampError::InvalidIndex)
261        ));
262    }
263
264    #[test]
265    fn test_bucket_for_address() {
266        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
267
268        let address = SwarmAddress::new([
269            0xCB, 0xE5, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
270            0, 0, 0, 0, 0, 0, 0,
271        ]);
272
273        assert_eq!(batch.bucket_for_address(&address), 0xCBE5);
274    }
275
276    #[test]
277    fn test_validate_bucket_match() {
278        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
279
280        let address = SwarmAddress::new([
281            0xCB, 0xE5, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
282            0, 0, 0, 0, 0, 0, 0,
283        ]);
284        let index = StampIndex::new(0xCBE5, 0);
285
286        assert!(batch.validate_bucket(&index, &address).is_ok());
287    }
288
289    #[test]
290    fn test_validate_bucket_mismatch() {
291        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
292
293        let address = SwarmAddress::new([
294            0xCB, 0xE5, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
295            0, 0, 0, 0, 0, 0, 0,
296        ]);
297        let index = StampIndex::new(0x1234, 0); // Wrong bucket
298
299        assert!(matches!(
300            batch.validate_bucket(&index, &address),
301            Err(StampError::BucketMismatch)
302        ));
303    }
304}