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}