Skip to main content

nectar_postage/
parallel.rs

1//! Parallel verification utilities.
2//!
3//! This module provides high-throughput parallel implementations for stamp verification
4//! using rayon parallel iterators.
5//!
6//! Verification is embarrassingly parallel - each stamp can be verified independently.
7//! We use rayon's parallel iterators to distribute verification across all available cores.
8//!
9//! # Performance Optimization
10//!
11//! For batches where you've already recovered the owner's public key, use
12//! [`verify_stamps_parallel_with_pubkey`] for approximately 2x faster verification
13//! compared to full ECDSA recovery.
14
15use alloy_primitives::Address;
16use alloy_signer::k256::ecdsa::VerifyingKey;
17use alloy_signer::utils::public_key_to_address;
18use rayon::prelude::*;
19
20use crate::{Stamp, StampDigest, StampError};
21use nectar_primitives::SwarmAddress;
22
23// Parallel Verification
24
25/// Result of a stamp verification.
26#[derive(Debug, Clone)]
27pub struct VerifyResult {
28    /// The index in the original input array.
29    pub index: usize,
30    /// The recovered signer address, or an error.
31    pub result: Result<Address, StampError>,
32}
33
34/// Verifies multiple stamps in parallel.
35///
36/// This function uses rayon to distribute verification across all available cores.
37/// Each stamp is verified by recovering the signer address from the signature.
38///
39/// # Arguments
40///
41/// * `stamps` - Slice of `(stamp, address)` tuples to verify
42///
43/// # Returns
44///
45/// A vector of verification results in the same order as the input.
46///
47/// # Example
48///
49/// ```ignore
50/// use nectar_postage::parallel::verify_stamps_parallel;
51///
52/// let stamps: Vec<Stamp> = /* ... */;
53/// let addresses: Vec<SwarmAddress> = /* ... */;
54///
55/// // Create tuples of references
56/// let items: Vec<_> = stamps.iter().zip(addresses.iter()).collect();
57/// let results = verify_stamps_parallel(&items);
58///
59/// for result in results {
60///     if let Ok(signer) = result.result {
61///         println!("Stamp {} signed by {}", result.index, signer);
62///     }
63/// }
64/// ```
65pub fn verify_stamps_parallel(stamps: &[(&Stamp, &SwarmAddress)]) -> Vec<VerifyResult> {
66    stamps
67        .par_iter()
68        .enumerate()
69        .map(|(index, (stamp, address))| {
70            let result = recover_stamp_signer(stamp, address);
71            VerifyResult { index, result }
72        })
73        .collect()
74}
75
76/// Verifies multiple stamps in parallel against an expected owner.
77///
78/// This is a convenience function that checks if all stamps were signed
79/// by the expected batch owner.
80///
81/// # Arguments
82///
83/// * `stamps` - Slice of `(stamp, address)` tuples to verify
84/// * `expected_owner` - The expected batch owner address
85///
86/// # Returns
87///
88/// A vector of verification results. Each result contains either the recovered
89/// address if the stamp is valid and signed by the expected owner, or an error.
90pub fn verify_stamps_parallel_with_owner(
91    stamps: &[(&Stamp, &SwarmAddress)],
92    expected_owner: Address,
93) -> Vec<VerifyResult> {
94    stamps
95        .par_iter()
96        .enumerate()
97        .map(|(index, (stamp, address))| {
98            let result = verify_stamp_owner(stamp, address, expected_owner);
99            VerifyResult { index, result }
100        })
101        .collect()
102}
103
104/// Verifies multiple stamps in parallel using a cached public key.
105///
106/// This is approximately 10x faster than [`verify_stamps_parallel`] because it
107/// avoids the expensive ECDSA public key recovery operation. Use this when you've
108/// already recovered the owner's public key from a previous stamp in the same batch.
109///
110/// # Arguments
111///
112/// * `stamps` - Slice of `(stamp, address)` tuples to verify
113/// * `owner_pubkey` - The cached owner public key (from a previous recovery)
114///
115/// # Returns
116///
117/// A vector of verification results. Each result contains either the owner address
118/// (derived from the public key) if verification succeeded, or an error.
119///
120/// # Example
121///
122/// ```ignore
123/// use nectar_postage::parallel::verify_stamps_parallel_with_pubkey;
124///
125/// // First, recover the public key from any stamp in the batch
126/// let pubkey = first_stamp.recover_pubkey(&first_address)?;
127///
128/// // Then verify all remaining stamps with the cached pubkey (~10x faster)
129/// let items: Vec<_> = stamps.iter().zip(addresses.iter()).collect();
130/// let results = verify_stamps_parallel_with_pubkey(&items, &pubkey);
131/// ```
132pub fn verify_stamps_parallel_with_pubkey(
133    stamps: &[(&Stamp, &SwarmAddress)],
134    owner_pubkey: &VerifyingKey,
135) -> Vec<VerifyResult> {
136    let owner_address = public_key_to_address(owner_pubkey);
137
138    stamps
139        .par_iter()
140        .enumerate()
141        .map(|(index, (stamp, address))| {
142            let result = match stamp.verify_with_pubkey(address, owner_pubkey) {
143                Ok(()) => Ok(owner_address),
144                Err(e) => Err(e),
145            };
146            VerifyResult { index, result }
147        })
148        .collect()
149}
150
151/// Recovers the signer address from a stamp.
152///
153/// Uses EIP-191 message recovery for interoperability.
154/// The prehash (keccak256 of stamp data) is treated as the message.
155fn recover_stamp_signer(stamp: &Stamp, address: &SwarmAddress) -> Result<Address, StampError> {
156    let digest = StampDigest::new(
157        *address,
158        stamp.batch(),
159        stamp.stamp_index(),
160        stamp.timestamp(),
161    );
162    let prehash = digest.to_prehash();
163
164    // Use recover_address_from_msg for EIP-191 compatibility
165    stamp
166        .signature()
167        .recover_address_from_msg(prehash.as_slice())
168        .map_err(|_| StampError::InvalidSignature)
169}
170
171/// Verifies a stamp was signed by the expected owner.
172fn verify_stamp_owner(
173    stamp: &Stamp,
174    address: &SwarmAddress,
175    expected_owner: Address,
176) -> Result<Address, StampError> {
177    let recovered = recover_stamp_signer(stamp, address)?;
178    if recovered != expected_owner {
179        return Err(StampError::OwnerMismatch {
180            expected: expected_owner,
181            actual: recovered,
182        });
183    }
184    Ok(recovered)
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use alloy_primitives::B256;
191    use alloy_signer::SignerSync;
192    use alloy_signer_local::PrivateKeySigner;
193
194    use crate::{Stamp, StampIndex, current_timestamp};
195
196    /// Creates a stamp for testing verification.
197    fn create_test_stamp(
198        signer: &PrivateKeySigner,
199        chunk_address: &SwarmAddress,
200        batch_id: B256,
201    ) -> Stamp {
202        let index = StampIndex::new(0, 0);
203        let timestamp = current_timestamp();
204        let digest = StampDigest::new(*chunk_address, batch_id, index, timestamp);
205        let prehash = digest.to_prehash();
206
207        // sign_message_sync returns alloy_primitives::Signature directly
208        let sig = signer.sign_message_sync(prehash.as_slice()).unwrap();
209        Stamp::with_index(batch_id, index, timestamp, sig)
210    }
211
212    #[test]
213    fn test_parallel_verification() {
214        let signer = PrivateKeySigner::random();
215        let expected_owner = signer.address();
216        let batch_id = B256::ZERO;
217
218        // Create stamps
219        let addresses: Vec<_> = (0..50)
220            .map(|_| SwarmAddress::from(B256::random()))
221            .collect();
222        let stamps: Vec<_> = addresses
223            .iter()
224            .map(|addr| create_test_stamp(&signer, addr, batch_id))
225            .collect();
226
227        // Verify stamps using tuple syntax
228        let verify_input: Vec<_> = stamps.iter().zip(addresses.iter()).collect();
229        let verify_results = verify_stamps_parallel_with_owner(&verify_input, expected_owner);
230
231        assert_eq!(verify_results.len(), 50);
232        for result in &verify_results {
233            assert!(result.result.is_ok());
234            assert_eq!(result.result.as_ref().unwrap(), &expected_owner);
235        }
236    }
237
238    #[test]
239    fn test_verify_wrong_signer() {
240        let signer = PrivateKeySigner::random();
241        let wrong_owner = Address::repeat_byte(0xFF);
242        let batch_id = B256::ZERO;
243
244        let address = SwarmAddress::from(B256::random());
245        let stamp = create_test_stamp(&signer, &address, batch_id);
246
247        // Use tuple syntax
248        let verify_input = [(&stamp, &address)];
249
250        let verify_results = verify_stamps_parallel_with_owner(&verify_input, wrong_owner);
251        assert!(matches!(
252            verify_results[0].result,
253            Err(StampError::OwnerMismatch { .. })
254        ));
255    }
256
257    #[test]
258    fn test_verify_stamps_parallel_basic() {
259        let signer = PrivateKeySigner::random();
260        let expected_owner = signer.address();
261        let batch_id = B256::ZERO;
262
263        let address = SwarmAddress::from(B256::random());
264        let stamp = create_test_stamp(&signer, &address, batch_id);
265
266        let verify_input = [(&stamp, &address)];
267        let results = verify_stamps_parallel(&verify_input);
268
269        assert_eq!(results.len(), 1);
270        assert_eq!(results[0].result.as_ref().unwrap(), &expected_owner);
271    }
272
273    #[test]
274    fn test_verify_stamps_parallel_with_pubkey() {
275        let signer = PrivateKeySigner::random();
276        let expected_owner = signer.address();
277        let batch_id = B256::ZERO;
278
279        // Create stamps
280        let addresses: Vec<_> = (0..50)
281            .map(|_| SwarmAddress::from(B256::random()))
282            .collect();
283        let stamps: Vec<_> = addresses
284            .iter()
285            .map(|addr| create_test_stamp(&signer, addr, batch_id))
286            .collect();
287
288        // Recover pubkey from first stamp
289        let pubkey = stamps[0].recover_pubkey(&addresses[0]).unwrap();
290
291        // Verify all stamps using cached pubkey
292        let verify_input: Vec<_> = stamps.iter().zip(addresses.iter()).collect();
293        let verify_results = verify_stamps_parallel_with_pubkey(&verify_input, &pubkey);
294
295        assert_eq!(verify_results.len(), 50);
296        for result in &verify_results {
297            assert!(result.result.is_ok());
298            assert_eq!(result.result.as_ref().unwrap(), &expected_owner);
299        }
300    }
301}