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);
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> StoreValidator<S> {
147    /// Validates a stamp.
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 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)?;
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 fn validate_structure(
172        &self,
173        stamp: &Stamp,
174        address: &SwarmAddress,
175    ) -> Result<(), StampError> {
176        let batch = self.get_batch_for_stamp(stamp)?;
177        self.validate_structure_with_batch(stamp, address, &batch)
178    }
179
180    /// Gets and validates the batch for a stamp.
181    fn get_batch_for_stamp(&self, stamp: &Stamp) -> Result<Batch, StampError> {
182        self.store
183            .get_usable(&stamp.batch(), self.confirmation_threshold)
184            .map_err(|e| match e {
185                crate::BatchStoreError::NotFound(id) => StampError::BatchNotFound(id),
186                crate::BatchStoreError::NotUsable {
187                    created,
188                    current,
189                    threshold,
190                    ..
191                } => StampError::BatchNotUsable {
192                    created,
193                    current,
194                    threshold,
195                },
196                crate::BatchStoreError::Expired {
197                    value,
198                    total_amount,
199                    ..
200                } => StampError::BatchExpired {
201                    value,
202                    total_amount,
203                },
204                crate::BatchStoreError::Store(_) => StampError::BatchNotFound(stamp.batch()),
205            })
206    }
207
208    /// Validates structure given an already-retrieved batch.
209    fn validate_structure_with_batch(
210        &self,
211        stamp: &Stamp,
212        address: &SwarmAddress,
213        batch: &Batch,
214    ) -> Result<(), StampError> {
215        // Validate index bounds
216        batch.validate_index(&stamp.stamp_index())?;
217
218        // Validate bucket matches address
219        batch.validate_bucket(&stamp.stamp_index(), address)?;
220
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use alloy_primitives::{Address, B256};
229
230    #[test]
231    fn test_validate_index_valid() {
232        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
233
234        // Valid: bucket < 2^16, index < 2^(18-16) = 4
235        let index = StampIndex::new(1000, 3);
236        assert!(batch.validate_index(&index).is_ok());
237    }
238
239    #[test]
240    fn test_validate_index_bucket_out_of_range() {
241        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
242
243        // Invalid: bucket >= 2^16 = 65536
244        let index = StampIndex::new(70000, 0);
245        assert!(matches!(
246            batch.validate_index(&index),
247            Err(StampError::InvalidIndex)
248        ));
249    }
250
251    #[test]
252    fn test_validate_index_position_out_of_range() {
253        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
254
255        // Invalid: index >= 2^(18-16) = 4
256        let index = StampIndex::new(1000, 5);
257        assert!(matches!(
258            batch.validate_index(&index),
259            Err(StampError::InvalidIndex)
260        ));
261    }
262
263    #[test]
264    fn test_bucket_for_address() {
265        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
266
267        let address = SwarmAddress::new([
268            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,
269            0, 0, 0, 0, 0, 0, 0,
270        ]);
271
272        assert_eq!(batch.bucket_for_address(&address), 0xCBE5);
273    }
274
275    #[test]
276    fn test_validate_bucket_match() {
277        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
278
279        let address = SwarmAddress::new([
280            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,
281            0, 0, 0, 0, 0, 0, 0,
282        ]);
283        let index = StampIndex::new(0xCBE5, 0);
284
285        assert!(batch.validate_bucket(&index, &address).is_ok());
286    }
287
288    #[test]
289    fn test_validate_bucket_mismatch() {
290        let batch = Batch::new(B256::ZERO, 0, 0, Address::ZERO, 18, 16, false);
291
292        let address = SwarmAddress::new([
293            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,
294            0, 0, 0, 0, 0, 0, 0,
295        ]);
296        let index = StampIndex::new(0x1234, 0); // Wrong bucket
297
298        assert!(matches!(
299            batch.validate_bucket(&index, &address),
300            Err(StampError::BucketMismatch)
301        ));
302    }
303}