Skip to main content

saorsa_node/payment/
verifier.rs

1//! Payment verifier with LRU cache and EVM verification.
2//!
3//! This is the core payment verification logic for saorsa-node.
4//! All new data requires EVM payment on Arbitrum (no free tier).
5
6use crate::error::{Error, Result};
7use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
8use crate::payment::proof::deserialize_proof;
9use crate::payment::quote::{verify_quote_content, verify_quote_signature};
10use crate::payment::single_node::REQUIRED_QUOTES;
11use ant_evm::{ProofOfPayment, RewardsAddress};
12use evmlib::contract::payment_vault::error::Error as PaymentVaultError;
13use evmlib::contract::payment_vault::verify_data_payment;
14use evmlib::Network as EvmNetwork;
15use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
16use std::time::SystemTime;
17use tracing::{debug, info};
18
19/// Minimum allowed size for a payment proof in bytes.
20///
21/// This minimum ensures the proof contains at least a basic cryptographic hash or identifier.
22/// Proofs smaller than this are rejected as they cannot contain sufficient payment information.
23const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
24
25/// Maximum allowed size for a payment proof in bytes (100 KB).
26///
27/// A `ProofOfPayment` with 5 ML-DSA-65 quotes can reach ~30 KB (each quote carries a
28/// ~1,952-byte public key and a 3,309-byte signature plus metadata). 100 KB provides
29/// headroom for future fields while still capping memory during verification.
30const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 102_400;
31
32/// Maximum age of a payment quote before it's considered expired (24 hours).
33/// Prevents replaying old cheap quotes against nearly-full nodes.
34const QUOTE_MAX_AGE_SECS: u64 = 86_400;
35
36/// Maximum allowed clock skew for quote timestamps (60 seconds).
37/// Accounts for NTP synchronization differences between P2P nodes.
38const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
39
40/// Configuration for EVM payment verification.
41///
42/// EVM verification is always on. All new data requires on-chain
43/// payment verification. The network field selects which EVM chain to use.
44#[derive(Debug, Clone)]
45pub struct EvmVerifierConfig {
46    /// EVM network to use (Arbitrum One, Arbitrum Sepolia, etc.)
47    pub network: EvmNetwork,
48}
49
50impl Default for EvmVerifierConfig {
51    fn default() -> Self {
52        Self {
53            network: EvmNetwork::ArbitrumOne,
54        }
55    }
56}
57
58/// Configuration for the payment verifier.
59///
60/// All new data requires EVM payment on Arbitrum. The cache stores
61/// previously verified payments to avoid redundant on-chain lookups.
62#[derive(Debug, Clone)]
63pub struct PaymentVerifierConfig {
64    /// EVM verifier configuration.
65    pub evm: EvmVerifierConfig,
66    /// Cache capacity (number of `XorName` values to cache).
67    pub cache_capacity: usize,
68    /// Local node's rewards address.
69    /// The verifier rejects payments that don't include this node as a recipient.
70    pub local_rewards_address: RewardsAddress,
71}
72
73/// Status returned by payment verification.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum PaymentStatus {
76    /// Data was found in local cache - previously paid.
77    CachedAsVerified,
78    /// New data - payment required.
79    PaymentRequired,
80    /// Payment was provided and verified.
81    PaymentVerified,
82}
83
84impl PaymentStatus {
85    /// Returns true if the data can be stored (cached or payment verified).
86    #[must_use]
87    pub fn can_store(&self) -> bool {
88        matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
89    }
90
91    /// Returns true if this status indicates the data was already paid for.
92    #[must_use]
93    pub fn is_cached(&self) -> bool {
94        matches!(self, Self::CachedAsVerified)
95    }
96}
97
98/// Main payment verifier for saorsa-node.
99///
100/// Uses:
101/// 1. LRU cache for fast lookups of previously verified `XorName` values
102/// 2. EVM payment verification for new data (always required)
103pub struct PaymentVerifier {
104    /// LRU cache of verified `XorName` values.
105    cache: VerifiedCache,
106    /// Configuration.
107    config: PaymentVerifierConfig,
108}
109
110impl PaymentVerifier {
111    /// Create a new payment verifier.
112    #[must_use]
113    pub fn new(config: PaymentVerifierConfig) -> Self {
114        let cache = VerifiedCache::with_capacity(config.cache_capacity);
115
116        let cache_capacity = config.cache_capacity;
117        info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on)");
118
119        Self { cache, config }
120    }
121
122    /// Check if payment is required for the given `XorName`.
123    ///
124    /// This is the main entry point for payment verification:
125    /// 1. Check LRU cache (fast path)
126    /// 2. If not cached, payment is required
127    ///
128    /// # Arguments
129    ///
130    /// * `xorname` - The content-addressed name of the data
131    ///
132    /// # Returns
133    ///
134    /// * `PaymentStatus::CachedAsVerified` - Found in local cache (previously paid)
135    /// * `PaymentStatus::PaymentRequired` - Not cached (payment required)
136    pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
137        // Check LRU cache (fast path)
138        if self.cache.contains(xorname) {
139            if tracing::enabled!(tracing::Level::DEBUG) {
140                debug!("Data {} found in verified cache", hex::encode(xorname));
141            }
142            return PaymentStatus::CachedAsVerified;
143        }
144
145        // Not in cache - payment required
146        if tracing::enabled!(tracing::Level::DEBUG) {
147            debug!(
148                "Data {} not in cache - payment required",
149                hex::encode(xorname)
150            );
151        }
152        PaymentStatus::PaymentRequired
153    }
154
155    /// Verify that a PUT request has valid payment.
156    ///
157    /// This is the complete payment verification flow:
158    /// 1. Check if data is in cache (previously paid)
159    /// 2. If not, verify the provided payment proof
160    ///
161    /// # Arguments
162    ///
163    /// * `xorname` - The content-addressed name of the data
164    /// * `payment_proof` - Optional payment proof (required if not in cache)
165    ///
166    /// # Returns
167    ///
168    /// * `Ok(PaymentStatus)` - Verification succeeded
169    /// * `Err(Error::Payment)` - No payment and not cached, or payment invalid
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if payment is required but not provided, or if payment is invalid.
174    pub async fn verify_payment(
175        &self,
176        xorname: &XorName,
177        payment_proof: Option<&[u8]>,
178    ) -> Result<PaymentStatus> {
179        // First check if payment is required
180        let status = self.check_payment_required(xorname);
181
182        match status {
183            PaymentStatus::CachedAsVerified => {
184                // No payment needed - already in cache
185                Ok(status)
186            }
187            PaymentStatus::PaymentRequired => {
188                // EVM verification is always on — verify the proof
189                if let Some(proof) = payment_proof {
190                    let proof_len = proof.len();
191                    if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
192                        return Err(Error::Payment(format!(
193                            "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
194                        )));
195                    }
196                    if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
197                        return Err(Error::Payment(format!(
198                            "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
199                        )));
200                    }
201
202                    // Deserialize the proof (supports both new PaymentProof and legacy ProofOfPayment)
203                    let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
204                        Error::Payment(format!("Failed to deserialize payment proof: {e}"))
205                    })?;
206
207                    if !tx_hashes.is_empty() {
208                        debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
209                    }
210
211                    // Verify the payment using EVM
212                    self.verify_evm_payment(xorname, &payment).await?;
213
214                    // Cache the verified xorname
215                    self.cache.insert(*xorname);
216
217                    Ok(PaymentStatus::PaymentVerified)
218                } else {
219                    // No payment provided in production mode
220                    Err(Error::Payment(format!(
221                        "Payment required for new data {}",
222                        hex::encode(xorname)
223                    )))
224                }
225            }
226            PaymentStatus::PaymentVerified => Err(Error::Payment(
227                "Unexpected PaymentVerified status from check_payment_required".to_string(),
228            )),
229        }
230    }
231
232    /// Get cache statistics.
233    #[must_use]
234    pub fn cache_stats(&self) -> CacheStats {
235        self.cache.stats()
236    }
237
238    /// Get the number of cached entries.
239    #[must_use]
240    pub fn cache_len(&self) -> usize {
241        self.cache.len()
242    }
243
244    /// Pre-populate the payment cache for a given address.
245    ///
246    /// This marks the address as already paid, so subsequent `verify_payment`
247    /// calls will return `CachedAsVerified` without on-chain verification.
248    /// Useful for test setups where real EVM payment is not needed.
249    #[cfg(any(test, feature = "test-utils"))]
250    pub fn cache_insert(&self, xorname: XorName) {
251        self.cache.insert(xorname);
252    }
253
254    /// Verify an EVM payment proof.
255    ///
256    /// This verification ALWAYS validates payment proofs on-chain.
257    /// It verifies that:
258    /// 1. All quotes target the correct content address (xorname binding)
259    /// 2. All quote ML-DSA-65 signatures are valid (offloaded to a blocking
260    ///    thread via `spawn_blocking` since post-quantum signature verification
261    ///    is CPU-intensive)
262    /// 3. The payment was made on-chain via the EVM payment vault contract
263    ///
264    /// For unit tests that don't need on-chain verification, pre-populate
265    /// the cache so `verify_payment` returns `CachedAsVerified` before
266    /// reaching this method.
267    async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
268        if tracing::enabled!(tracing::Level::DEBUG) {
269            let xorname_hex = hex::encode(xorname);
270            let quote_count = payment.peer_quotes.len();
271            debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
272        }
273
274        Self::validate_quote_structure(payment)?;
275        Self::validate_quote_content(payment, xorname)?;
276        Self::validate_quote_timestamps(payment)?;
277        Self::validate_peer_bindings(payment)?;
278        self.validate_local_recipient(payment)?;
279
280        // Verify quote signatures (CPU-bound, run off async runtime)
281        let peer_quotes = payment.peer_quotes.clone();
282        tokio::task::spawn_blocking(move || {
283            for (encoded_peer_id, quote) in &peer_quotes {
284                if !verify_quote_signature(quote) {
285                    return Err(Error::Payment(
286                        format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
287                    ));
288                }
289            }
290            Ok(())
291        })
292        .await
293        .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
294
295        // Verify on-chain payment
296        let payment_digest = payment.digest();
297        if payment_digest.is_empty() {
298            return Err(Error::Payment("Payment has no quotes".to_string()));
299        }
300
301        let owned_quote_hashes = vec![];
302        match verify_data_payment(&self.config.evm.network, owned_quote_hashes, payment_digest)
303            .await
304        {
305            Ok(_amount) => {
306                if tracing::enabled!(tracing::Level::INFO) {
307                    info!("EVM payment verified for {}", hex::encode(xorname));
308                }
309                Ok(())
310            }
311            Err(PaymentVaultError::PaymentInvalid) => Err(Error::Payment(format!(
312                "Payment verification failed on-chain for {}",
313                hex::encode(xorname)
314            ))),
315            Err(e) => Err(Error::Payment(format!(
316                "EVM verification error for {}: {e}",
317                hex::encode(xorname)
318            ))),
319        }
320    }
321
322    /// Validate quote count, uniqueness, and basic structure.
323    fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
324        if payment.peer_quotes.is_empty() {
325            return Err(Error::Payment("Payment has no quotes".to_string()));
326        }
327
328        let quote_count = payment.peer_quotes.len();
329        if quote_count != REQUIRED_QUOTES {
330            return Err(Error::Payment(format!(
331                "Payment must have exactly {REQUIRED_QUOTES} quotes, got {quote_count}"
332            )));
333        }
334
335        let mut seen: Vec<&ant_evm::EncodedPeerId> = Vec::with_capacity(quote_count);
336        for (encoded_peer_id, _) in &payment.peer_quotes {
337            if seen.contains(&encoded_peer_id) {
338                return Err(Error::Payment(format!(
339                    "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
340                )));
341            }
342            seen.push(encoded_peer_id);
343        }
344
345        Ok(())
346    }
347
348    /// Verify all quotes target the correct content address.
349    fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
350        for (encoded_peer_id, quote) in &payment.peer_quotes {
351            if !verify_quote_content(quote, xorname) {
352                return Err(Error::Payment(format!(
353                    "Quote content address mismatch for peer {encoded_peer_id:?}: expected {}, got {}",
354                    hex::encode(xorname),
355                    hex::encode(quote.content.0)
356                )));
357            }
358        }
359        Ok(())
360    }
361
362    /// Verify quote freshness — reject stale or excessively future quotes.
363    fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
364        let now = SystemTime::now();
365        for (encoded_peer_id, quote) in &payment.peer_quotes {
366            match now.duration_since(quote.timestamp) {
367                Ok(age) => {
368                    if age.as_secs() > QUOTE_MAX_AGE_SECS {
369                        return Err(Error::Payment(format!(
370                            "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
371                            age.as_secs()
372                        )));
373                    }
374                }
375                Err(_) => {
376                    if let Ok(skew) = quote.timestamp.duration_since(now) {
377                        if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
378                            return Err(Error::Payment(format!(
379                                "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
380                                 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
381                                skew.as_secs()
382                            )));
383                        }
384                    } else {
385                        return Err(Error::Payment(format!(
386                            "Quote from peer {encoded_peer_id:?} has invalid timestamp"
387                        )));
388                    }
389                }
390            }
391        }
392        Ok(())
393    }
394
395    /// Verify each quote's `pub_key` matches the claimed peer ID via BLAKE3.
396    fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
397        for (encoded_peer_id, quote) in &payment.peer_quotes {
398            let expected_peer_id = peer_id_from_public_key_bytes(&quote.pub_key)
399                .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
400
401            let libp2p_peer_id = encoded_peer_id
402                .to_peer_id()
403                .map_err(|e| Error::Payment(format!("Invalid encoded peer ID: {e}")))?;
404            let peer_id_bytes = libp2p_peer_id.to_bytes();
405            let raw_peer_bytes = if peer_id_bytes.len() > 2 {
406                &peer_id_bytes[2..]
407            } else {
408                return Err(Error::Payment(format!(
409                    "Invalid encoded peer ID: too short ({} bytes)",
410                    peer_id_bytes.len()
411                )));
412            };
413
414            if expected_peer_id.as_bytes() != raw_peer_bytes {
415                return Err(Error::Payment(format!(
416                    "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
417                     BLAKE3(pub_key) = {}, peer_id = {}",
418                    expected_peer_id.to_hex(),
419                    hex::encode(raw_peer_bytes)
420                )));
421            }
422        }
423        Ok(())
424    }
425
426    /// Verify this node is among the paid recipients.
427    fn validate_local_recipient(&self, payment: &ProofOfPayment) -> Result<()> {
428        let local_addr = &self.config.local_rewards_address;
429        let is_recipient = payment
430            .peer_quotes
431            .iter()
432            .any(|(_, quote)| quote.rewards_address == *local_addr);
433        if !is_recipient {
434            return Err(Error::Payment(
435                "Payment proof does not include this node as a recipient".to_string(),
436            ));
437        }
438        Ok(())
439    }
440}
441
442#[cfg(test)]
443#[allow(clippy::expect_used)]
444mod tests {
445    use super::*;
446
447    /// Create a verifier for unit tests. EVM is always on, but tests can
448    /// pre-populate the cache to bypass on-chain verification.
449    fn create_test_verifier() -> PaymentVerifier {
450        let config = PaymentVerifierConfig {
451            evm: EvmVerifierConfig::default(),
452            cache_capacity: 100,
453            local_rewards_address: RewardsAddress::new([1u8; 20]),
454        };
455        PaymentVerifier::new(config)
456    }
457
458    #[test]
459    fn test_payment_required_for_new_data() {
460        let verifier = create_test_verifier();
461        let xorname = [1u8; 32];
462
463        // All uncached data requires payment
464        let status = verifier.check_payment_required(&xorname);
465        assert_eq!(status, PaymentStatus::PaymentRequired);
466    }
467
468    #[test]
469    fn test_cache_hit() {
470        let verifier = create_test_verifier();
471        let xorname = [1u8; 32];
472
473        // Manually add to cache
474        verifier.cache.insert(xorname);
475
476        // Should return CachedAsVerified
477        let status = verifier.check_payment_required(&xorname);
478        assert_eq!(status, PaymentStatus::CachedAsVerified);
479    }
480
481    #[tokio::test]
482    async fn test_verify_payment_without_proof_rejected() {
483        let verifier = create_test_verifier();
484        let xorname = [1u8; 32];
485
486        // No proof provided => should return an error (EVM is always on)
487        let result = verifier.verify_payment(&xorname, None).await;
488        assert!(
489            result.is_err(),
490            "Expected Err without proof, got: {result:?}"
491        );
492    }
493
494    #[tokio::test]
495    async fn test_verify_payment_cached() {
496        let verifier = create_test_verifier();
497        let xorname = [1u8; 32];
498
499        // Add to cache — simulates previously-paid data
500        verifier.cache.insert(xorname);
501
502        // Should succeed without payment (cached)
503        let result = verifier.verify_payment(&xorname, None).await;
504        assert!(result.is_ok());
505        assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
506    }
507
508    #[test]
509    fn test_payment_status_can_store() {
510        assert!(PaymentStatus::CachedAsVerified.can_store());
511        assert!(PaymentStatus::PaymentVerified.can_store());
512        assert!(!PaymentStatus::PaymentRequired.can_store());
513    }
514
515    #[test]
516    fn test_payment_status_is_cached() {
517        assert!(PaymentStatus::CachedAsVerified.is_cached());
518        assert!(!PaymentStatus::PaymentVerified.is_cached());
519        assert!(!PaymentStatus::PaymentRequired.is_cached());
520    }
521
522    #[tokio::test]
523    async fn test_cache_preload_bypasses_evm() {
524        let verifier = create_test_verifier();
525        let xorname = [42u8; 32];
526
527        // Not yet cached — should require payment
528        assert_eq!(
529            verifier.check_payment_required(&xorname),
530            PaymentStatus::PaymentRequired
531        );
532
533        // Pre-populate cache (simulates a previous successful payment)
534        verifier.cache.insert(xorname);
535
536        // Now the xorname should be cached
537        assert_eq!(
538            verifier.check_payment_required(&xorname),
539            PaymentStatus::CachedAsVerified
540        );
541    }
542
543    #[tokio::test]
544    async fn test_proof_too_small() {
545        let verifier = create_test_verifier();
546        let xorname = [1u8; 32];
547
548        // Proof smaller than MIN_PAYMENT_PROOF_SIZE_BYTES
549        let small_proof = vec![0u8; MIN_PAYMENT_PROOF_SIZE_BYTES - 1];
550        let result = verifier.verify_payment(&xorname, Some(&small_proof)).await;
551        assert!(result.is_err());
552        let err_msg = format!("{}", result.expect_err("should fail"));
553        assert!(
554            err_msg.contains("too small"),
555            "Error should mention 'too small': {err_msg}"
556        );
557    }
558
559    #[tokio::test]
560    async fn test_proof_too_large() {
561        let verifier = create_test_verifier();
562        let xorname = [2u8; 32];
563
564        // Proof larger than MAX_PAYMENT_PROOF_SIZE_BYTES
565        let large_proof = vec![0u8; MAX_PAYMENT_PROOF_SIZE_BYTES + 1];
566        let result = verifier.verify_payment(&xorname, Some(&large_proof)).await;
567        assert!(result.is_err());
568        let err_msg = format!("{}", result.expect_err("should fail"));
569        assert!(
570            err_msg.contains("too large"),
571            "Error should mention 'too large': {err_msg}"
572        );
573    }
574
575    #[tokio::test]
576    async fn test_proof_at_min_boundary() {
577        let verifier = create_test_verifier();
578        let xorname = [3u8; 32];
579
580        // Exactly MIN_PAYMENT_PROOF_SIZE_BYTES — passes size check, but
581        // will fail deserialization (not valid msgpack)
582        let boundary_proof = vec![0xFFu8; MIN_PAYMENT_PROOF_SIZE_BYTES];
583        let result = verifier
584            .verify_payment(&xorname, Some(&boundary_proof))
585            .await;
586        assert!(result.is_err());
587        let err_msg = format!("{}", result.expect_err("should fail deser"));
588        assert!(
589            err_msg.contains("deserialize"),
590            "Error should mention deserialization: {err_msg}"
591        );
592    }
593
594    #[tokio::test]
595    async fn test_proof_at_max_boundary() {
596        let verifier = create_test_verifier();
597        let xorname = [4u8; 32];
598
599        // Exactly MAX_PAYMENT_PROOF_SIZE_BYTES — passes size check, but
600        // will fail deserialization
601        let boundary_proof = vec![0xFFu8; MAX_PAYMENT_PROOF_SIZE_BYTES];
602        let result = verifier
603            .verify_payment(&xorname, Some(&boundary_proof))
604            .await;
605        assert!(result.is_err());
606        let err_msg = format!("{}", result.expect_err("should fail deser"));
607        assert!(
608            err_msg.contains("deserialize"),
609            "Error should mention deserialization: {err_msg}"
610        );
611    }
612
613    #[tokio::test]
614    async fn test_malformed_msgpack_proof() {
615        let verifier = create_test_verifier();
616        let xorname = [5u8; 32];
617
618        // Valid size but garbage bytes — should fail deserialization
619        let garbage = vec![0xAB; 64];
620        let result = verifier.verify_payment(&xorname, Some(&garbage)).await;
621        assert!(result.is_err());
622        let err_msg = format!("{}", result.expect_err("should fail"));
623        assert!(err_msg.contains("deserialize"));
624    }
625
626    #[test]
627    fn test_cache_len_getter() {
628        let verifier = create_test_verifier();
629        assert_eq!(verifier.cache_len(), 0);
630
631        verifier.cache.insert([10u8; 32]);
632        assert_eq!(verifier.cache_len(), 1);
633
634        verifier.cache.insert([20u8; 32]);
635        assert_eq!(verifier.cache_len(), 2);
636    }
637
638    #[test]
639    fn test_cache_stats_after_operations() {
640        let verifier = create_test_verifier();
641        let xorname = [7u8; 32];
642
643        // Miss
644        verifier.check_payment_required(&xorname);
645        let stats = verifier.cache_stats();
646        assert_eq!(stats.misses, 1);
647        assert_eq!(stats.hits, 0);
648
649        // Insert and hit
650        verifier.cache.insert(xorname);
651        verifier.check_payment_required(&xorname);
652        let stats = verifier.cache_stats();
653        assert_eq!(stats.hits, 1);
654        assert_eq!(stats.misses, 1);
655        assert_eq!(stats.additions, 1);
656    }
657
658    #[tokio::test]
659    async fn test_concurrent_cache_lookups() {
660        let verifier = std::sync::Arc::new(create_test_verifier());
661
662        // Pre-populate cache for all 10 xornames
663        for i in 0..10u8 {
664            verifier.cache.insert([i; 32]);
665        }
666
667        let mut handles = Vec::new();
668        for i in 0..10u8 {
669            let v = verifier.clone();
670            handles.push(tokio::spawn(async move {
671                let xorname = [i; 32];
672                v.verify_payment(&xorname, None).await
673            }));
674        }
675
676        for handle in handles {
677            let result = handle.await.expect("task panicked");
678            assert!(result.is_ok());
679            assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
680        }
681
682        assert_eq!(verifier.cache_len(), 10);
683    }
684
685    #[test]
686    fn test_default_evm_config() {
687        let _config = EvmVerifierConfig::default();
688        // EVM is always on — default network is ArbitrumOne
689    }
690
691    #[test]
692    fn test_real_ml_dsa_proof_size_within_limits() {
693        use crate::payment::metrics::QuotingMetricsTracker;
694        use crate::payment::proof::PaymentProof;
695        use crate::payment::quote::{QuoteGenerator, XorName};
696        use alloy::primitives::FixedBytes;
697        use ant_evm::{EncodedPeerId, RewardsAddress};
698        use saorsa_core::MlDsa65;
699        use saorsa_pqc::pqc::types::MlDsaSecretKey;
700        use saorsa_pqc::pqc::MlDsaOperations;
701
702        let ml_dsa = MlDsa65::new();
703        let mut peer_quotes = Vec::new();
704
705        for i in 0..5u8 {
706            let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
707
708            let rewards_address = RewardsAddress::new([i; 20]);
709            let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
710            let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
711
712            let pub_key_bytes = public_key.as_bytes().to_vec();
713            let sk_bytes = secret_key.as_bytes().to_vec();
714            generator.set_signer(pub_key_bytes, move |msg| {
715                let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
716                let ml_dsa = MlDsa65::new();
717                ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
718            });
719
720            let content: XorName = [i; 32];
721            let quote = generator.create_quote(content, 4096, 0).expect("quote");
722
723            let keypair = libp2p::identity::Keypair::generate_ed25519();
724            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
725            peer_quotes.push((EncodedPeerId::from(peer_id), quote));
726        }
727
728        let proof = PaymentProof {
729            proof_of_payment: ProofOfPayment { peer_quotes },
730            tx_hashes: vec![FixedBytes::from([0xABu8; 32])],
731        };
732
733        let proof_bytes = rmp_serde::to_vec(&proof).expect("serialize");
734
735        // 5 ML-DSA-65 quotes with ~1952-byte pub keys and ~3309-byte signatures
736        // should produce a proof in the 20-60 KB range
737        assert!(
738            proof_bytes.len() > 20_000,
739            "Real 5-quote ML-DSA proof should be > 20 KB, got {} bytes",
740            proof_bytes.len()
741        );
742        assert!(
743            proof_bytes.len() < MAX_PAYMENT_PROOF_SIZE_BYTES,
744            "Real 5-quote ML-DSA proof ({} bytes) should fit within {} byte limit",
745            proof_bytes.len(),
746            MAX_PAYMENT_PROOF_SIZE_BYTES
747        );
748    }
749
750    #[tokio::test]
751    async fn test_content_address_mismatch_rejected() {
752        use crate::payment::proof::PaymentProof;
753        use ant_evm::{EncodedPeerId, PaymentQuote, QuotingMetrics, RewardsAddress};
754        use libp2p::identity::Keypair;
755        use libp2p::PeerId;
756        use std::time::SystemTime;
757
758        let verifier = create_test_verifier();
759
760        // The xorname we're trying to store
761        let target_xorname = [0xAAu8; 32];
762
763        // Create a quote for a DIFFERENT xorname
764        let wrong_xorname = [0xBBu8; 32];
765        let quote = PaymentQuote {
766            content: xor_name::XorName(wrong_xorname),
767            timestamp: SystemTime::now(),
768            quoting_metrics: QuotingMetrics {
769                data_size: 1024,
770                data_type: 0,
771                close_records_stored: 0,
772                records_per_type: vec![],
773                max_records: 1000,
774                received_payment_count: 0,
775                live_time: 0,
776                network_density: None,
777                network_size: None,
778            },
779            rewards_address: RewardsAddress::new([1u8; 20]),
780            pub_key: vec![0u8; 64],
781            signature: vec![0u8; 64],
782        };
783
784        // Build 5 quotes with distinct peer IDs (required by REQUIRED_QUOTES enforcement)
785        let mut peer_quotes = Vec::new();
786        for _ in 0..5 {
787            let keypair = Keypair::generate_ed25519();
788            let peer_id = PeerId::from_public_key(&keypair.public());
789            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
790        }
791        let payment = ProofOfPayment { peer_quotes };
792
793        let proof = PaymentProof {
794            proof_of_payment: payment,
795            tx_hashes: vec![],
796        };
797
798        let proof_bytes = rmp_serde::to_vec(&proof).expect("serialize proof");
799
800        let result = verifier
801            .verify_payment(&target_xorname, Some(&proof_bytes))
802            .await;
803
804        assert!(result.is_err(), "Should reject mismatched content address");
805        let err_msg = format!("{}", result.expect_err("should be error"));
806        assert!(
807            err_msg.contains("content address mismatch"),
808            "Error should mention 'content address mismatch': {err_msg}"
809        );
810    }
811
812    /// Helper: create a fake quote with the given xorname and timestamp.
813    fn make_fake_quote(
814        xorname: [u8; 32],
815        timestamp: SystemTime,
816        rewards_address: RewardsAddress,
817    ) -> ant_evm::PaymentQuote {
818        use ant_evm::{PaymentQuote, QuotingMetrics};
819
820        PaymentQuote {
821            content: xor_name::XorName(xorname),
822            timestamp,
823            quoting_metrics: QuotingMetrics {
824                data_size: 1024,
825                data_type: 0,
826                close_records_stored: 0,
827                records_per_type: vec![],
828                max_records: 1000,
829                received_payment_count: 0,
830                live_time: 0,
831                network_density: None,
832                network_size: None,
833            },
834            rewards_address,
835            pub_key: vec![0u8; 64],
836            signature: vec![0u8; 64],
837        }
838    }
839
840    /// Helper: wrap quotes into a serialized `PaymentProof`.
841    fn serialize_proof(
842        peer_quotes: Vec<(ant_evm::EncodedPeerId, ant_evm::PaymentQuote)>,
843    ) -> Vec<u8> {
844        use crate::payment::proof::PaymentProof;
845
846        let proof = PaymentProof {
847            proof_of_payment: ProofOfPayment { peer_quotes },
848            tx_hashes: vec![],
849        };
850        rmp_serde::to_vec(&proof).expect("serialize proof")
851    }
852
853    #[tokio::test]
854    async fn test_expired_quote_rejected() {
855        use ant_evm::{EncodedPeerId, RewardsAddress};
856        use std::time::Duration;
857
858        let verifier = create_test_verifier();
859        let xorname = [0xCCu8; 32];
860        let rewards_addr = RewardsAddress::new([1u8; 20]);
861
862        // Create a quote that's 25 hours old (exceeds 24-hour max)
863        let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
864        let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
865
866        let mut peer_quotes = Vec::new();
867        for _ in 0..5 {
868            let keypair = libp2p::identity::Keypair::generate_ed25519();
869            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
870            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
871        }
872
873        let proof_bytes = serialize_proof(peer_quotes);
874        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
875
876        assert!(result.is_err(), "Should reject expired quote");
877        let err_msg = format!("{}", result.expect_err("should fail"));
878        assert!(
879            err_msg.contains("expired"),
880            "Error should mention 'expired': {err_msg}"
881        );
882    }
883
884    #[tokio::test]
885    async fn test_future_timestamp_rejected() {
886        use ant_evm::{EncodedPeerId, RewardsAddress};
887        use std::time::Duration;
888
889        let verifier = create_test_verifier();
890        let xorname = [0xDDu8; 32];
891        let rewards_addr = RewardsAddress::new([1u8; 20]);
892
893        // Create a quote with a timestamp 1 hour in the future
894        let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
895        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
896
897        let mut peer_quotes = Vec::new();
898        for _ in 0..5 {
899            let keypair = libp2p::identity::Keypair::generate_ed25519();
900            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
901            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
902        }
903
904        let proof_bytes = serialize_proof(peer_quotes);
905        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
906
907        assert!(result.is_err(), "Should reject future-timestamped quote");
908        let err_msg = format!("{}", result.expect_err("should fail"));
909        assert!(
910            err_msg.contains("future"),
911            "Error should mention 'future': {err_msg}"
912        );
913    }
914
915    #[tokio::test]
916    async fn test_quote_within_clock_skew_tolerance_accepted() {
917        use ant_evm::{EncodedPeerId, RewardsAddress};
918        use std::time::Duration;
919
920        let verifier = create_test_verifier();
921        let xorname = [0xD1u8; 32];
922        let rewards_addr = RewardsAddress::new([1u8; 20]);
923
924        // Quote 30 seconds in the future — within 60s tolerance
925        let future_timestamp = SystemTime::now() + Duration::from_secs(30);
926        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
927
928        let mut peer_quotes = Vec::new();
929        for _ in 0..5 {
930            let keypair = libp2p::identity::Keypair::generate_ed25519();
931            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
932            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
933        }
934
935        let proof_bytes = serialize_proof(peer_quotes);
936        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
937
938        // Should NOT fail at timestamp check (will fail later at pub_key binding)
939        let err_msg = format!("{}", result.expect_err("should fail at later check"));
940        assert!(
941            !err_msg.contains("future"),
942            "Should pass timestamp check (within tolerance), but got: {err_msg}"
943        );
944    }
945
946    #[tokio::test]
947    async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
948        use ant_evm::{EncodedPeerId, RewardsAddress};
949        use std::time::Duration;
950
951        let verifier = create_test_verifier();
952        let xorname = [0xD2u8; 32];
953        let rewards_addr = RewardsAddress::new([1u8; 20]);
954
955        // Quote 120 seconds in the future — exceeds 60s tolerance
956        let future_timestamp = SystemTime::now() + Duration::from_secs(120);
957        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
958
959        let mut peer_quotes = Vec::new();
960        for _ in 0..5 {
961            let keypair = libp2p::identity::Keypair::generate_ed25519();
962            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
963            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
964        }
965
966        let proof_bytes = serialize_proof(peer_quotes);
967        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
968
969        assert!(
970            result.is_err(),
971            "Should reject quote beyond clock skew tolerance"
972        );
973        let err_msg = format!("{}", result.expect_err("should fail"));
974        assert!(
975            err_msg.contains("future"),
976            "Error should mention 'future': {err_msg}"
977        );
978    }
979
980    #[tokio::test]
981    async fn test_quote_23h_old_still_accepted() {
982        use ant_evm::{EncodedPeerId, RewardsAddress};
983        use std::time::Duration;
984
985        let verifier = create_test_verifier();
986        let xorname = [0xD3u8; 32];
987        let rewards_addr = RewardsAddress::new([1u8; 20]);
988
989        // Quote 23 hours old — within 24h max age
990        let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
991        let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
992
993        let mut peer_quotes = Vec::new();
994        for _ in 0..5 {
995            let keypair = libp2p::identity::Keypair::generate_ed25519();
996            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
997            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
998        }
999
1000        let proof_bytes = serialize_proof(peer_quotes);
1001        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1002
1003        // Should NOT fail at timestamp check (will fail later at pub_key binding)
1004        let err_msg = format!("{}", result.expect_err("should fail at later check"));
1005        assert!(
1006            !err_msg.contains("expired"),
1007            "Should pass expiry check (23h < 24h), but got: {err_msg}"
1008        );
1009    }
1010
1011    /// Helper: build an `EncodedPeerId` that matches the BLAKE3 hash of an ML-DSA public key.
1012    fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> ant_evm::EncodedPeerId {
1013        let saorsa_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1014        // Wrap raw 32-byte peer ID in identity multihash format: [0x00, length, ...bytes]
1015        let raw = saorsa_peer_id.as_bytes();
1016        let mut multihash_bytes = Vec::with_capacity(2 + raw.len());
1017        multihash_bytes.push(0x00); // identity multihash code
1018                                    // PeerId is always 32 bytes, safely fits in u8
1019        multihash_bytes.push(u8::try_from(raw.len()).unwrap_or(32));
1020        multihash_bytes.extend_from_slice(raw);
1021        let libp2p_peer_id =
1022            libp2p::PeerId::from_bytes(&multihash_bytes).expect("valid multihash peer ID");
1023        ant_evm::EncodedPeerId::from(libp2p_peer_id)
1024    }
1025
1026    #[tokio::test]
1027    async fn test_local_not_in_paid_set_rejected() {
1028        use ant_evm::RewardsAddress;
1029        use saorsa_core::MlDsa65;
1030        use saorsa_pqc::pqc::MlDsaOperations;
1031
1032        // Verifier with a local rewards address set
1033        let local_addr = RewardsAddress::new([0xAAu8; 20]);
1034        let config = PaymentVerifierConfig {
1035            evm: EvmVerifierConfig {
1036                network: EvmNetwork::ArbitrumOne,
1037            },
1038            cache_capacity: 100,
1039            local_rewards_address: local_addr,
1040        };
1041        let verifier = PaymentVerifier::new(config);
1042
1043        let xorname = [0xEEu8; 32];
1044        // Quotes pay a DIFFERENT rewards address
1045        let other_addr = RewardsAddress::new([0xBBu8; 20]);
1046
1047        // Use real ML-DSA keys so the pub_key→peer_id binding check passes
1048        let ml_dsa = MlDsa65::new();
1049        let mut peer_quotes = Vec::new();
1050        for _ in 0..5 {
1051            let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1052            let pub_key_bytes = public_key.as_bytes().to_vec();
1053            let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1054
1055            let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1056            quote.pub_key = pub_key_bytes;
1057
1058            peer_quotes.push((encoded, quote));
1059        }
1060
1061        let proof_bytes = serialize_proof(peer_quotes);
1062        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1063
1064        assert!(result.is_err(), "Should reject payment not addressed to us");
1065        let err_msg = format!("{}", result.expect_err("should fail"));
1066        assert!(
1067            err_msg.contains("does not include this node as a recipient"),
1068            "Error should mention recipient rejection: {err_msg}"
1069        );
1070    }
1071
1072    #[tokio::test]
1073    async fn test_wrong_peer_binding_rejected() {
1074        use ant_evm::{EncodedPeerId, RewardsAddress};
1075        use saorsa_core::MlDsa65;
1076        use saorsa_pqc::pqc::MlDsaOperations;
1077
1078        let verifier = create_test_verifier();
1079        let xorname = [0xFFu8; 32];
1080        let rewards_addr = RewardsAddress::new([1u8; 20]);
1081
1082        // Generate a real ML-DSA keypair so pub_key is valid
1083        let ml_dsa = MlDsa65::new();
1084        let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1085        let pub_key_bytes = public_key.as_bytes().to_vec();
1086
1087        // Create a quote with a real pub_key but attach it to a random peer ID
1088        // whose identity multihash does NOT match BLAKE3(pub_key)
1089        let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1090        quote.pub_key = pub_key_bytes;
1091
1092        // Use random ed25519 peer IDs — they won't match BLAKE3(pub_key)
1093        let mut peer_quotes = Vec::new();
1094        for _ in 0..5 {
1095            let keypair = libp2p::identity::Keypair::generate_ed25519();
1096            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1097            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1098        }
1099
1100        let proof_bytes = serialize_proof(peer_quotes);
1101        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1102
1103        assert!(result.is_err(), "Should reject wrong peer binding");
1104        let err_msg = format!("{}", result.expect_err("should fail"));
1105        assert!(
1106            err_msg.contains("pub_key does not belong to claimed peer"),
1107            "Error should mention binding mismatch: {err_msg}"
1108        );
1109    }
1110}