1use 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
19const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
24
25const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 102_400;
31
32const QUOTE_MAX_AGE_SECS: u64 = 86_400;
35
36const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
39
40#[derive(Debug, Clone)]
45pub struct EvmVerifierConfig {
46 pub network: EvmNetwork,
48}
49
50impl Default for EvmVerifierConfig {
51 fn default() -> Self {
52 Self {
53 network: EvmNetwork::ArbitrumOne,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
63pub struct PaymentVerifierConfig {
64 pub evm: EvmVerifierConfig,
66 pub cache_capacity: usize,
68 pub local_rewards_address: RewardsAddress,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum PaymentStatus {
76 CachedAsVerified,
78 PaymentRequired,
80 PaymentVerified,
82}
83
84impl PaymentStatus {
85 #[must_use]
87 pub fn can_store(&self) -> bool {
88 matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
89 }
90
91 #[must_use]
93 pub fn is_cached(&self) -> bool {
94 matches!(self, Self::CachedAsVerified)
95 }
96}
97
98pub struct PaymentVerifier {
104 cache: VerifiedCache,
106 config: PaymentVerifierConfig,
108}
109
110impl PaymentVerifier {
111 #[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 pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
137 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 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 pub async fn verify_payment(
175 &self,
176 xorname: &XorName,
177 payment_proof: Option<&[u8]>,
178 ) -> Result<PaymentStatus> {
179 let status = self.check_payment_required(xorname);
181
182 match status {
183 PaymentStatus::CachedAsVerified => {
184 Ok(status)
186 }
187 PaymentStatus::PaymentRequired => {
188 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 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 self.verify_evm_payment(xorname, &payment).await?;
213
214 self.cache.insert(*xorname);
216
217 Ok(PaymentStatus::PaymentVerified)
218 } else {
219 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 #[must_use]
234 pub fn cache_stats(&self) -> CacheStats {
235 self.cache.stats()
236 }
237
238 #[must_use]
240 pub fn cache_len(&self) -> usize {
241 self.cache.len()
242 }
243
244 #[cfg(any(test, feature = "test-utils"))]
250 pub fn cache_insert(&self, xorname: XorName) {
251 self.cache.insert(xorname);
252 }
253
254 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 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 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 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 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 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 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("e.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 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 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 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 verifier.cache.insert(xorname);
475
476 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 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 verifier.cache.insert(xorname);
501
502 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 assert_eq!(
529 verifier.check_payment_required(&xorname),
530 PaymentStatus::PaymentRequired
531 );
532
533 verifier.cache.insert(xorname);
535
536 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 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 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 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 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 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 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 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 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 }
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 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 let target_xorname = [0xAAu8; 32];
762
763 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 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 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 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 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 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 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 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 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 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 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 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 let raw = saorsa_peer_id.as_bytes();
1016 let mut multihash_bytes = Vec::with_capacity(2 + raw.len());
1017 multihash_bytes.push(0x00); 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 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 let other_addr = RewardsAddress::new([0xBBu8; 20]);
1046
1047 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 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 let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1090 quote.pub_key = pub_key_bytes;
1091
1092 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}