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}