1use crate::error::{Error, Result};
7use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
8use crate::payment::proof::{
9 deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
10};
11use crate::payment::quote::{verify_quote_content, verify_quote_signature};
12use crate::payment::single_node::REQUIRED_QUOTES;
13use ant_evm::merkle_payments::OnChainPaymentInfo;
14use ant_evm::{ProofOfPayment, RewardsAddress};
15use evmlib::contract::merkle_payment_vault;
16use evmlib::merkle_batch_payment::PoolHash;
17use evmlib::Network as EvmNetwork;
18use lru::LruCache;
19use parking_lot::Mutex;
20use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
21use std::num::NonZeroUsize;
22use std::time::SystemTime;
23use tracing::{debug, info};
24
25const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
30
31const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 262_144;
38
39const QUOTE_MAX_AGE_SECS: u64 = 86_400;
42
43const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
46
47#[derive(Debug, Clone)]
52pub struct EvmVerifierConfig {
53 pub network: EvmNetwork,
55}
56
57impl Default for EvmVerifierConfig {
58 fn default() -> Self {
59 Self {
60 network: EvmNetwork::ArbitrumOne,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
70pub struct PaymentVerifierConfig {
71 pub evm: EvmVerifierConfig,
73 pub cache_capacity: usize,
75 pub local_rewards_address: RewardsAddress,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PaymentStatus {
83 CachedAsVerified,
85 PaymentRequired,
87 PaymentVerified,
89}
90
91impl PaymentStatus {
92 #[must_use]
94 pub fn can_store(&self) -> bool {
95 matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
96 }
97
98 #[must_use]
100 pub fn is_cached(&self) -> bool {
101 matches!(self, Self::CachedAsVerified)
102 }
103}
104
105const DEFAULT_POOL_CACHE_CAPACITY: usize = 1_000;
107
108pub struct PaymentVerifier {
115 cache: VerifiedCache,
117 pool_cache: Mutex<LruCache<PoolHash, OnChainPaymentInfo>>,
119 config: PaymentVerifierConfig,
121}
122
123impl PaymentVerifier {
124 #[must_use]
126 pub fn new(config: PaymentVerifierConfig) -> Self {
127 const _: () = assert!(
128 DEFAULT_POOL_CACHE_CAPACITY > 0,
129 "pool cache capacity must be > 0"
130 );
131 let cache = VerifiedCache::with_capacity(config.cache_capacity);
132 let pool_cache_size =
133 NonZeroUsize::new(DEFAULT_POOL_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN);
134 let pool_cache = Mutex::new(LruCache::new(pool_cache_size));
135
136 let cache_capacity = config.cache_capacity;
137 info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on, pool_cache={DEFAULT_POOL_CACHE_CAPACITY})");
138
139 Self {
140 cache,
141 pool_cache,
142 config,
143 }
144 }
145
146 pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
161 if self.cache.contains(xorname) {
163 if tracing::enabled!(tracing::Level::DEBUG) {
164 debug!("Data {} found in verified cache", hex::encode(xorname));
165 }
166 return PaymentStatus::CachedAsVerified;
167 }
168
169 if tracing::enabled!(tracing::Level::DEBUG) {
171 debug!(
172 "Data {} not in cache - payment required",
173 hex::encode(xorname)
174 );
175 }
176 PaymentStatus::PaymentRequired
177 }
178
179 pub async fn verify_payment(
199 &self,
200 xorname: &XorName,
201 payment_proof: Option<&[u8]>,
202 ) -> Result<PaymentStatus> {
203 let status = self.check_payment_required(xorname);
205
206 match status {
207 PaymentStatus::CachedAsVerified => {
208 Ok(status)
210 }
211 PaymentStatus::PaymentRequired => {
212 if let Some(proof) = payment_proof {
214 let proof_len = proof.len();
215 if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
216 return Err(Error::Payment(format!(
217 "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
218 )));
219 }
220 if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
221 return Err(Error::Payment(format!(
222 "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
223 )));
224 }
225
226 match detect_proof_type(proof) {
228 Some(ProofType::Merkle) => {
229 self.verify_merkle_payment(xorname, proof).await?;
230 }
231 Some(ProofType::SingleNode) => {
232 let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
233 Error::Payment(format!("Failed to deserialize payment proof: {e}"))
234 })?;
235
236 if !tx_hashes.is_empty() {
237 debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
238 }
239
240 self.verify_evm_payment(xorname, &payment).await?;
241 }
242 None => {
243 let tag = proof.first().copied().unwrap_or(0);
244 return Err(Error::Payment(format!(
245 "Unknown payment proof type tag: 0x{tag:02x}"
246 )));
247 }
248 }
249
250 self.cache.insert(*xorname);
252
253 Ok(PaymentStatus::PaymentVerified)
254 } else {
255 Err(Error::Payment(format!(
257 "Payment required for new data {}",
258 hex::encode(xorname)
259 )))
260 }
261 }
262 PaymentStatus::PaymentVerified => Err(Error::Payment(
263 "Unexpected PaymentVerified status from check_payment_required".to_string(),
264 )),
265 }
266 }
267
268 #[must_use]
270 pub fn cache_stats(&self) -> CacheStats {
271 self.cache.stats()
272 }
273
274 #[must_use]
276 pub fn cache_len(&self) -> usize {
277 self.cache.len()
278 }
279
280 #[cfg(any(test, feature = "test-utils"))]
286 pub fn cache_insert(&self, xorname: XorName) {
287 self.cache.insert(xorname);
288 }
289
290 async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
304 if tracing::enabled!(tracing::Level::DEBUG) {
305 let xorname_hex = hex::encode(xorname);
306 let quote_count = payment.peer_quotes.len();
307 debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
308 }
309
310 Self::validate_quote_structure(payment)?;
311 Self::validate_quote_content(payment, xorname)?;
312 Self::validate_quote_timestamps(payment)?;
313 Self::validate_peer_bindings(payment)?;
314 self.validate_local_recipient(payment)?;
315
316 let peer_quotes = payment.peer_quotes.clone();
318 tokio::task::spawn_blocking(move || {
319 for (encoded_peer_id, quote) in &peer_quotes {
320 if !verify_quote_signature(quote) {
321 return Err(Error::Payment(
322 format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
323 ));
324 }
325 }
326 Ok(())
327 })
328 .await
329 .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
330
331 let payment_digest = payment.digest();
347 if payment_digest.is_empty() {
348 return Err(Error::Payment("Payment has no quotes".to_string()));
349 }
350
351 let payment_verifications: Vec<_> = payment_digest
352 .into_iter()
353 .map(
354 evmlib::contract::payment_vault::interface::IPaymentVault::PaymentVerification::from,
355 )
356 .collect();
357
358 let provider = evmlib::utils::http_provider(self.config.evm.network.rpc_url().clone());
359 let handler = evmlib::contract::payment_vault::handler::PaymentVaultHandler::new(
360 *self.config.evm.network.data_payments_address(),
361 provider,
362 );
363
364 let results = handler
365 .verify_payment(payment_verifications)
366 .await
367 .map_err(|e| {
368 Error::Payment(format!(
369 "EVM verification error for {}: {e}",
370 hex::encode(xorname)
371 ))
372 })?;
373
374 let paid_results: Vec<_> = results
375 .iter()
376 .filter(|r| r.amountPaid > evmlib::common::U256::ZERO)
377 .collect();
378
379 if paid_results.is_empty() {
380 return Err(Error::Payment(format!(
381 "Payment verification failed on-chain for {} (no paid quotes found)",
382 hex::encode(xorname)
383 )));
384 }
385
386 for result in &paid_results {
387 if !result.isValid {
388 return Err(Error::Payment(format!(
389 "Payment verification failed on-chain for {} (paid quote is invalid)",
390 hex::encode(xorname)
391 )));
392 }
393 }
394
395 if tracing::enabled!(tracing::Level::INFO) {
396 let valid_count = paid_results.len();
397 info!(
398 "EVM payment verified for {} ({valid_count} paid and valid, {} total results)",
399 hex::encode(xorname),
400 results.len()
401 );
402 }
403 Ok(())
404 }
405
406 fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
408 if payment.peer_quotes.is_empty() {
409 return Err(Error::Payment("Payment has no quotes".to_string()));
410 }
411
412 let quote_count = payment.peer_quotes.len();
413 if quote_count != REQUIRED_QUOTES {
414 return Err(Error::Payment(format!(
415 "Payment must have exactly {REQUIRED_QUOTES} quotes, got {quote_count}"
416 )));
417 }
418
419 let mut seen: Vec<&ant_evm::EncodedPeerId> = Vec::with_capacity(quote_count);
420 for (encoded_peer_id, _) in &payment.peer_quotes {
421 if seen.contains(&encoded_peer_id) {
422 return Err(Error::Payment(format!(
423 "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
424 )));
425 }
426 seen.push(encoded_peer_id);
427 }
428
429 Ok(())
430 }
431
432 fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
434 for (encoded_peer_id, quote) in &payment.peer_quotes {
435 if !verify_quote_content(quote, xorname) {
436 return Err(Error::Payment(format!(
437 "Quote content address mismatch for peer {encoded_peer_id:?}: expected {}, got {}",
438 hex::encode(xorname),
439 hex::encode(quote.content.0)
440 )));
441 }
442 }
443 Ok(())
444 }
445
446 fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
448 let now = SystemTime::now();
449 for (encoded_peer_id, quote) in &payment.peer_quotes {
450 match now.duration_since(quote.timestamp) {
451 Ok(age) => {
452 if age.as_secs() > QUOTE_MAX_AGE_SECS {
453 return Err(Error::Payment(format!(
454 "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
455 age.as_secs()
456 )));
457 }
458 }
459 Err(_) => {
460 if let Ok(skew) = quote.timestamp.duration_since(now) {
461 if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
462 return Err(Error::Payment(format!(
463 "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
464 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
465 skew.as_secs()
466 )));
467 }
468 } else {
469 return Err(Error::Payment(format!(
470 "Quote from peer {encoded_peer_id:?} has invalid timestamp"
471 )));
472 }
473 }
474 }
475 }
476 Ok(())
477 }
478
479 fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
481 for (encoded_peer_id, quote) in &payment.peer_quotes {
482 let expected_peer_id = peer_id_from_public_key_bytes("e.pub_key)
483 .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
484
485 let libp2p_peer_id = encoded_peer_id
486 .to_peer_id()
487 .map_err(|e| Error::Payment(format!("Invalid encoded peer ID: {e}")))?;
488 let peer_id_bytes = libp2p_peer_id.to_bytes();
489 let raw_peer_bytes = if peer_id_bytes.len() > 2 {
490 &peer_id_bytes[2..]
491 } else {
492 return Err(Error::Payment(format!(
493 "Invalid encoded peer ID: too short ({} bytes)",
494 peer_id_bytes.len()
495 )));
496 };
497
498 if expected_peer_id.as_bytes() != raw_peer_bytes {
499 return Err(Error::Payment(format!(
500 "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
501 BLAKE3(pub_key) = {}, peer_id = {}",
502 expected_peer_id.to_hex(),
503 hex::encode(raw_peer_bytes)
504 )));
505 }
506 }
507 Ok(())
508 }
509
510 #[allow(clippy::too_many_lines)]
519 async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> {
520 if tracing::enabled!(tracing::Level::DEBUG) {
521 debug!("Verifying merkle payment for {}", hex::encode(xorname));
522 }
523
524 let merkle_proof = deserialize_merkle_proof(proof_bytes)
526 .map_err(|e| Error::Payment(format!("Failed to deserialize merkle proof: {e}")))?;
527
528 if merkle_proof.address.0 != *xorname {
530 return Err(Error::Payment(format!(
531 "Merkle proof address mismatch: proof is for {}, but storing {}",
532 hex::encode(merkle_proof.address.0),
533 hex::encode(xorname)
534 )));
535 }
536
537 let pool_hash = merkle_proof.winner_pool_hash();
538
539 let cached_info = {
541 let mut pool_cache = self.pool_cache.lock();
542 pool_cache.get(&pool_hash).cloned()
543 };
544
545 let payment_info = if let Some(info) = cached_info {
546 debug!("Pool cache hit for hash {}", hex::encode(pool_hash));
547 info
548 } else {
549 let info =
551 merkle_payment_vault::get_merkle_payment_info(&self.config.evm.network, pool_hash)
552 .await
553 .map_err(|e| {
554 Error::Payment(format!(
555 "Failed to query merkle payment info for pool {}: {e}",
556 hex::encode(pool_hash)
557 ))
558 })?;
559
560 let paid_node_addresses: Vec<_> = info
561 .paidNodeAddresses
562 .iter()
563 .map(|pna| (pna.rewardsAddress, usize::from(pna.poolIndex)))
564 .collect();
565
566 let on_chain_info = OnChainPaymentInfo {
567 depth: info.depth,
568 merkle_payment_timestamp: info.merklePaymentTimestamp,
569 paid_node_addresses,
570 };
571
572 {
574 let mut pool_cache = self.pool_cache.lock();
575 pool_cache.put(pool_hash, on_chain_info.clone());
576 }
577
578 debug!(
579 "Queried on-chain merkle payment info for pool {}: depth={}, timestamp={}, paid_nodes={}",
580 hex::encode(pool_hash),
581 on_chain_info.depth,
582 on_chain_info.merkle_payment_timestamp,
583 on_chain_info.paid_node_addresses.len()
584 );
585
586 on_chain_info
587 };
588
589 for candidate in &merkle_proof.winner_pool.candidate_nodes {
597 if !crate::payment::verify_merkle_candidate_signature(candidate) {
598 return Err(Error::Payment(format!(
599 "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})",
600 candidate.reward_address
601 )));
602 }
603 if candidate.merkle_payment_timestamp != payment_info.merkle_payment_timestamp {
604 return Err(Error::Payment(format!(
605 "Candidate timestamp mismatch: expected {}, got {} (reward: {})",
606 payment_info.merkle_payment_timestamp,
607 candidate.merkle_payment_timestamp,
608 candidate.reward_address
609 )));
610 }
611 }
612
613 let smart_contract_root = merkle_proof.winner_pool.midpoint_proof.root();
615
616 ant_evm::merkle_payments::verify_merkle_proof(
619 &merkle_proof.address,
620 &merkle_proof.data_proof,
621 &merkle_proof.winner_pool.midpoint_proof,
622 payment_info.depth,
623 smart_contract_root,
624 payment_info.merkle_payment_timestamp,
625 )
626 .map_err(|e| {
627 Error::Payment(format!(
628 "Merkle proof verification failed for {}: {e}",
629 hex::encode(xorname)
630 ))
631 })?;
632
633 if payment_info.paid_node_addresses.len() != payment_info.depth as usize {
635 return Err(Error::Payment(format!(
636 "Wrong number of paid nodes: expected {}, got {}",
637 payment_info.depth,
638 payment_info.paid_node_addresses.len()
639 )));
640 }
641
642 for (addr, idx) in &payment_info.paid_node_addresses {
652 let node = merkle_proof
653 .winner_pool
654 .candidate_nodes
655 .get(*idx)
656 .ok_or_else(|| {
657 Error::Payment(format!(
658 "Paid node index {idx} out of bounds for pool size {}",
659 merkle_proof.winner_pool.candidate_nodes.len()
660 ))
661 })?;
662 if node.reward_address != *addr {
663 return Err(Error::Payment(format!(
664 "Paid node address mismatch at index {idx}: expected {addr}, got {}",
665 node.reward_address
666 )));
667 }
668 }
669
670 if tracing::enabled!(tracing::Level::INFO) {
671 info!(
672 "Merkle payment verified for {} (pool: {})",
673 hex::encode(xorname),
674 hex::encode(pool_hash)
675 );
676 }
677
678 Ok(())
679 }
680
681 fn validate_local_recipient(&self, payment: &ProofOfPayment) -> Result<()> {
683 let local_addr = &self.config.local_rewards_address;
684 let is_recipient = payment
685 .peer_quotes
686 .iter()
687 .any(|(_, quote)| quote.rewards_address == *local_addr);
688 if !is_recipient {
689 return Err(Error::Payment(
690 "Payment proof does not include this node as a recipient".to_string(),
691 ));
692 }
693 Ok(())
694 }
695}
696
697#[cfg(test)]
698#[allow(clippy::expect_used)]
699mod tests {
700 use super::*;
701
702 fn create_test_verifier() -> PaymentVerifier {
705 let config = PaymentVerifierConfig {
706 evm: EvmVerifierConfig::default(),
707 cache_capacity: 100,
708 local_rewards_address: RewardsAddress::new([1u8; 20]),
709 };
710 PaymentVerifier::new(config)
711 }
712
713 #[test]
714 fn test_payment_required_for_new_data() {
715 let verifier = create_test_verifier();
716 let xorname = [1u8; 32];
717
718 let status = verifier.check_payment_required(&xorname);
720 assert_eq!(status, PaymentStatus::PaymentRequired);
721 }
722
723 #[test]
724 fn test_cache_hit() {
725 let verifier = create_test_verifier();
726 let xorname = [1u8; 32];
727
728 verifier.cache.insert(xorname);
730
731 let status = verifier.check_payment_required(&xorname);
733 assert_eq!(status, PaymentStatus::CachedAsVerified);
734 }
735
736 #[tokio::test]
737 async fn test_verify_payment_without_proof_rejected() {
738 let verifier = create_test_verifier();
739 let xorname = [1u8; 32];
740
741 let result = verifier.verify_payment(&xorname, None).await;
743 assert!(
744 result.is_err(),
745 "Expected Err without proof, got: {result:?}"
746 );
747 }
748
749 #[tokio::test]
750 async fn test_verify_payment_cached() {
751 let verifier = create_test_verifier();
752 let xorname = [1u8; 32];
753
754 verifier.cache.insert(xorname);
756
757 let result = verifier.verify_payment(&xorname, None).await;
759 assert!(result.is_ok());
760 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
761 }
762
763 #[test]
764 fn test_payment_status_can_store() {
765 assert!(PaymentStatus::CachedAsVerified.can_store());
766 assert!(PaymentStatus::PaymentVerified.can_store());
767 assert!(!PaymentStatus::PaymentRequired.can_store());
768 }
769
770 #[test]
771 fn test_payment_status_is_cached() {
772 assert!(PaymentStatus::CachedAsVerified.is_cached());
773 assert!(!PaymentStatus::PaymentVerified.is_cached());
774 assert!(!PaymentStatus::PaymentRequired.is_cached());
775 }
776
777 #[tokio::test]
778 async fn test_cache_preload_bypasses_evm() {
779 let verifier = create_test_verifier();
780 let xorname = [42u8; 32];
781
782 assert_eq!(
784 verifier.check_payment_required(&xorname),
785 PaymentStatus::PaymentRequired
786 );
787
788 verifier.cache.insert(xorname);
790
791 assert_eq!(
793 verifier.check_payment_required(&xorname),
794 PaymentStatus::CachedAsVerified
795 );
796 }
797
798 #[tokio::test]
799 async fn test_proof_too_small() {
800 let verifier = create_test_verifier();
801 let xorname = [1u8; 32];
802
803 let small_proof = vec![0u8; MIN_PAYMENT_PROOF_SIZE_BYTES - 1];
805 let result = verifier.verify_payment(&xorname, Some(&small_proof)).await;
806 assert!(result.is_err());
807 let err_msg = format!("{}", result.expect_err("should fail"));
808 assert!(
809 err_msg.contains("too small"),
810 "Error should mention 'too small': {err_msg}"
811 );
812 }
813
814 #[tokio::test]
815 async fn test_proof_too_large() {
816 let verifier = create_test_verifier();
817 let xorname = [2u8; 32];
818
819 let large_proof = vec![0u8; MAX_PAYMENT_PROOF_SIZE_BYTES + 1];
821 let result = verifier.verify_payment(&xorname, Some(&large_proof)).await;
822 assert!(result.is_err());
823 let err_msg = format!("{}", result.expect_err("should fail"));
824 assert!(
825 err_msg.contains("too large"),
826 "Error should mention 'too large': {err_msg}"
827 );
828 }
829
830 #[tokio::test]
831 async fn test_proof_at_min_boundary_unknown_tag() {
832 let verifier = create_test_verifier();
833 let xorname = [3u8; 32];
834
835 let boundary_proof = vec![0xFFu8; MIN_PAYMENT_PROOF_SIZE_BYTES];
837 let result = verifier
838 .verify_payment(&xorname, Some(&boundary_proof))
839 .await;
840 assert!(result.is_err());
841 let err_msg = format!("{}", result.expect_err("should fail"));
842 assert!(
843 err_msg.contains("Unknown payment proof type tag"),
844 "Error should mention unknown tag: {err_msg}"
845 );
846 }
847
848 #[tokio::test]
849 async fn test_proof_at_max_boundary_unknown_tag() {
850 let verifier = create_test_verifier();
851 let xorname = [4u8; 32];
852
853 let boundary_proof = vec![0xFFu8; MAX_PAYMENT_PROOF_SIZE_BYTES];
855 let result = verifier
856 .verify_payment(&xorname, Some(&boundary_proof))
857 .await;
858 assert!(result.is_err());
859 let err_msg = format!("{}", result.expect_err("should fail"));
860 assert!(
861 err_msg.contains("Unknown payment proof type tag"),
862 "Error should mention unknown tag: {err_msg}"
863 );
864 }
865
866 #[tokio::test]
867 async fn test_malformed_single_node_proof() {
868 let verifier = create_test_verifier();
869 let xorname = [5u8; 32];
870
871 let mut garbage = vec![crate::ant_protocol::PROOF_TAG_SINGLE_NODE];
873 garbage.extend_from_slice(&[0xAB; 63]);
874 let result = verifier.verify_payment(&xorname, Some(&garbage)).await;
875 assert!(result.is_err());
876 let err_msg = format!("{}", result.expect_err("should fail"));
877 assert!(
878 err_msg.contains("deserialize") || err_msg.contains("Failed"),
879 "Error should mention deserialization failure: {err_msg}"
880 );
881 }
882
883 #[test]
884 fn test_cache_len_getter() {
885 let verifier = create_test_verifier();
886 assert_eq!(verifier.cache_len(), 0);
887
888 verifier.cache.insert([10u8; 32]);
889 assert_eq!(verifier.cache_len(), 1);
890
891 verifier.cache.insert([20u8; 32]);
892 assert_eq!(verifier.cache_len(), 2);
893 }
894
895 #[test]
896 fn test_cache_stats_after_operations() {
897 let verifier = create_test_verifier();
898 let xorname = [7u8; 32];
899
900 verifier.check_payment_required(&xorname);
902 let stats = verifier.cache_stats();
903 assert_eq!(stats.misses, 1);
904 assert_eq!(stats.hits, 0);
905
906 verifier.cache.insert(xorname);
908 verifier.check_payment_required(&xorname);
909 let stats = verifier.cache_stats();
910 assert_eq!(stats.hits, 1);
911 assert_eq!(stats.misses, 1);
912 assert_eq!(stats.additions, 1);
913 }
914
915 #[tokio::test]
916 async fn test_concurrent_cache_lookups() {
917 let verifier = std::sync::Arc::new(create_test_verifier());
918
919 for i in 0..10u8 {
921 verifier.cache.insert([i; 32]);
922 }
923
924 let mut handles = Vec::new();
925 for i in 0..10u8 {
926 let v = verifier.clone();
927 handles.push(tokio::spawn(async move {
928 let xorname = [i; 32];
929 v.verify_payment(&xorname, None).await
930 }));
931 }
932
933 for handle in handles {
934 let result = handle.await.expect("task panicked");
935 assert!(result.is_ok());
936 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
937 }
938
939 assert_eq!(verifier.cache_len(), 10);
940 }
941
942 #[test]
943 fn test_default_evm_config() {
944 let _config = EvmVerifierConfig::default();
945 }
947
948 #[test]
949 fn test_real_ml_dsa_proof_size_within_limits() {
950 use crate::payment::metrics::QuotingMetricsTracker;
951 use crate::payment::proof::PaymentProof;
952 use crate::payment::quote::{QuoteGenerator, XorName};
953 use alloy::primitives::FixedBytes;
954 use ant_evm::{EncodedPeerId, RewardsAddress};
955 use saorsa_core::MlDsa65;
956 use saorsa_pqc::pqc::types::MlDsaSecretKey;
957 use saorsa_pqc::pqc::MlDsaOperations;
958
959 let ml_dsa = MlDsa65::new();
960 let mut peer_quotes = Vec::new();
961
962 for i in 0..5u8 {
963 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
964
965 let rewards_address = RewardsAddress::new([i; 20]);
966 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
967 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
968
969 let pub_key_bytes = public_key.as_bytes().to_vec();
970 let sk_bytes = secret_key.as_bytes().to_vec();
971 generator.set_signer(pub_key_bytes, move |msg| {
972 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
973 let ml_dsa = MlDsa65::new();
974 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
975 });
976
977 let content: XorName = [i; 32];
978 let quote = generator.create_quote(content, 4096, 0).expect("quote");
979
980 let keypair = libp2p::identity::Keypair::generate_ed25519();
981 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
982 peer_quotes.push((EncodedPeerId::from(peer_id), quote));
983 }
984
985 let proof = PaymentProof {
986 proof_of_payment: ProofOfPayment { peer_quotes },
987 tx_hashes: vec![FixedBytes::from([0xABu8; 32])],
988 };
989
990 let proof_bytes =
991 crate::payment::proof::serialize_single_node_proof(&proof).expect("serialize");
992
993 assert!(
996 proof_bytes.len() > 20_000,
997 "Real 5-quote ML-DSA proof should be > 20 KB, got {} bytes",
998 proof_bytes.len()
999 );
1000 assert!(
1001 proof_bytes.len() < MAX_PAYMENT_PROOF_SIZE_BYTES,
1002 "Real 5-quote ML-DSA proof ({} bytes) should fit within {} byte limit",
1003 proof_bytes.len(),
1004 MAX_PAYMENT_PROOF_SIZE_BYTES
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn test_content_address_mismatch_rejected() {
1010 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1011 use ant_evm::{EncodedPeerId, PaymentQuote, QuotingMetrics, RewardsAddress};
1012 use libp2p::identity::Keypair;
1013 use libp2p::PeerId;
1014 use std::time::SystemTime;
1015
1016 let verifier = create_test_verifier();
1017
1018 let target_xorname = [0xAAu8; 32];
1020
1021 let wrong_xorname = [0xBBu8; 32];
1023 let quote = PaymentQuote {
1024 content: xor_name::XorName(wrong_xorname),
1025 timestamp: SystemTime::now(),
1026 quoting_metrics: QuotingMetrics {
1027 data_size: 1024,
1028 data_type: 0,
1029 close_records_stored: 0,
1030 records_per_type: vec![],
1031 max_records: 1000,
1032 received_payment_count: 0,
1033 live_time: 0,
1034 network_density: None,
1035 network_size: None,
1036 },
1037 rewards_address: RewardsAddress::new([1u8; 20]),
1038 pub_key: vec![0u8; 64],
1039 signature: vec![0u8; 64],
1040 };
1041
1042 let mut peer_quotes = Vec::new();
1044 for _ in 0..5 {
1045 let keypair = Keypair::generate_ed25519();
1046 let peer_id = PeerId::from_public_key(&keypair.public());
1047 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1048 }
1049
1050 let proof = PaymentProof {
1051 proof_of_payment: ProofOfPayment { peer_quotes },
1052 tx_hashes: vec![],
1053 };
1054
1055 let proof_bytes = serialize_single_node_proof(&proof).expect("serialize proof");
1056
1057 let result = verifier
1058 .verify_payment(&target_xorname, Some(&proof_bytes))
1059 .await;
1060
1061 assert!(result.is_err(), "Should reject mismatched content address");
1062 let err_msg = format!("{}", result.expect_err("should be error"));
1063 assert!(
1064 err_msg.contains("content address mismatch"),
1065 "Error should mention 'content address mismatch': {err_msg}"
1066 );
1067 }
1068
1069 fn make_fake_quote(
1071 xorname: [u8; 32],
1072 timestamp: SystemTime,
1073 rewards_address: RewardsAddress,
1074 ) -> ant_evm::PaymentQuote {
1075 use ant_evm::{PaymentQuote, QuotingMetrics};
1076
1077 PaymentQuote {
1078 content: xor_name::XorName(xorname),
1079 timestamp,
1080 quoting_metrics: QuotingMetrics {
1081 data_size: 1024,
1082 data_type: 0,
1083 close_records_stored: 0,
1084 records_per_type: vec![],
1085 max_records: 1000,
1086 received_payment_count: 0,
1087 live_time: 0,
1088 network_density: None,
1089 network_size: None,
1090 },
1091 rewards_address,
1092 pub_key: vec![0u8; 64],
1093 signature: vec![0u8; 64],
1094 }
1095 }
1096
1097 fn serialize_proof(
1099 peer_quotes: Vec<(ant_evm::EncodedPeerId, ant_evm::PaymentQuote)>,
1100 ) -> Vec<u8> {
1101 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1102
1103 let proof = PaymentProof {
1104 proof_of_payment: ProofOfPayment { peer_quotes },
1105 tx_hashes: vec![],
1106 };
1107 serialize_single_node_proof(&proof).expect("serialize proof")
1108 }
1109
1110 #[tokio::test]
1111 async fn test_expired_quote_rejected() {
1112 use ant_evm::{EncodedPeerId, RewardsAddress};
1113 use std::time::Duration;
1114
1115 let verifier = create_test_verifier();
1116 let xorname = [0xCCu8; 32];
1117 let rewards_addr = RewardsAddress::new([1u8; 20]);
1118
1119 let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
1121 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1122
1123 let mut peer_quotes = Vec::new();
1124 for _ in 0..5 {
1125 let keypair = libp2p::identity::Keypair::generate_ed25519();
1126 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1127 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1128 }
1129
1130 let proof_bytes = serialize_proof(peer_quotes);
1131 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1132
1133 assert!(result.is_err(), "Should reject expired quote");
1134 let err_msg = format!("{}", result.expect_err("should fail"));
1135 assert!(
1136 err_msg.contains("expired"),
1137 "Error should mention 'expired': {err_msg}"
1138 );
1139 }
1140
1141 #[tokio::test]
1142 async fn test_future_timestamp_rejected() {
1143 use ant_evm::{EncodedPeerId, RewardsAddress};
1144 use std::time::Duration;
1145
1146 let verifier = create_test_verifier();
1147 let xorname = [0xDDu8; 32];
1148 let rewards_addr = RewardsAddress::new([1u8; 20]);
1149
1150 let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
1152 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1153
1154 let mut peer_quotes = Vec::new();
1155 for _ in 0..5 {
1156 let keypair = libp2p::identity::Keypair::generate_ed25519();
1157 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1158 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1159 }
1160
1161 let proof_bytes = serialize_proof(peer_quotes);
1162 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1163
1164 assert!(result.is_err(), "Should reject future-timestamped quote");
1165 let err_msg = format!("{}", result.expect_err("should fail"));
1166 assert!(
1167 err_msg.contains("future"),
1168 "Error should mention 'future': {err_msg}"
1169 );
1170 }
1171
1172 #[tokio::test]
1173 async fn test_quote_within_clock_skew_tolerance_accepted() {
1174 use ant_evm::{EncodedPeerId, RewardsAddress};
1175 use std::time::Duration;
1176
1177 let verifier = create_test_verifier();
1178 let xorname = [0xD1u8; 32];
1179 let rewards_addr = RewardsAddress::new([1u8; 20]);
1180
1181 let future_timestamp = SystemTime::now() + Duration::from_secs(30);
1183 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1184
1185 let mut peer_quotes = Vec::new();
1186 for _ in 0..5 {
1187 let keypair = libp2p::identity::Keypair::generate_ed25519();
1188 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1189 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1190 }
1191
1192 let proof_bytes = serialize_proof(peer_quotes);
1193 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1194
1195 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1197 assert!(
1198 !err_msg.contains("future"),
1199 "Should pass timestamp check (within tolerance), but got: {err_msg}"
1200 );
1201 }
1202
1203 #[tokio::test]
1204 async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
1205 use ant_evm::{EncodedPeerId, RewardsAddress};
1206 use std::time::Duration;
1207
1208 let verifier = create_test_verifier();
1209 let xorname = [0xD2u8; 32];
1210 let rewards_addr = RewardsAddress::new([1u8; 20]);
1211
1212 let future_timestamp = SystemTime::now() + Duration::from_secs(120);
1214 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1215
1216 let mut peer_quotes = Vec::new();
1217 for _ in 0..5 {
1218 let keypair = libp2p::identity::Keypair::generate_ed25519();
1219 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1220 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1221 }
1222
1223 let proof_bytes = serialize_proof(peer_quotes);
1224 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1225
1226 assert!(
1227 result.is_err(),
1228 "Should reject quote beyond clock skew tolerance"
1229 );
1230 let err_msg = format!("{}", result.expect_err("should fail"));
1231 assert!(
1232 err_msg.contains("future"),
1233 "Error should mention 'future': {err_msg}"
1234 );
1235 }
1236
1237 #[tokio::test]
1238 async fn test_quote_23h_old_still_accepted() {
1239 use ant_evm::{EncodedPeerId, RewardsAddress};
1240 use std::time::Duration;
1241
1242 let verifier = create_test_verifier();
1243 let xorname = [0xD3u8; 32];
1244 let rewards_addr = RewardsAddress::new([1u8; 20]);
1245
1246 let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
1248 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1249
1250 let mut peer_quotes = Vec::new();
1251 for _ in 0..5 {
1252 let keypair = libp2p::identity::Keypair::generate_ed25519();
1253 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1254 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1255 }
1256
1257 let proof_bytes = serialize_proof(peer_quotes);
1258 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1259
1260 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1262 assert!(
1263 !err_msg.contains("expired"),
1264 "Should pass expiry check (23h < 24h), but got: {err_msg}"
1265 );
1266 }
1267
1268 fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> ant_evm::EncodedPeerId {
1270 let ant_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1271 let raw = ant_peer_id.as_bytes();
1273 let mut multihash_bytes = Vec::with_capacity(2 + raw.len());
1274 multihash_bytes.push(0x00); multihash_bytes.push(u8::try_from(raw.len()).unwrap_or(32));
1277 multihash_bytes.extend_from_slice(raw);
1278 let libp2p_peer_id =
1279 libp2p::PeerId::from_bytes(&multihash_bytes).expect("valid multihash peer ID");
1280 ant_evm::EncodedPeerId::from(libp2p_peer_id)
1281 }
1282
1283 #[tokio::test]
1284 async fn test_local_not_in_paid_set_rejected() {
1285 use ant_evm::RewardsAddress;
1286 use saorsa_core::MlDsa65;
1287 use saorsa_pqc::pqc::MlDsaOperations;
1288
1289 let local_addr = RewardsAddress::new([0xAAu8; 20]);
1291 let config = PaymentVerifierConfig {
1292 evm: EvmVerifierConfig {
1293 network: EvmNetwork::ArbitrumOne,
1294 },
1295 cache_capacity: 100,
1296 local_rewards_address: local_addr,
1297 };
1298 let verifier = PaymentVerifier::new(config);
1299
1300 let xorname = [0xEEu8; 32];
1301 let other_addr = RewardsAddress::new([0xBBu8; 20]);
1303
1304 let ml_dsa = MlDsa65::new();
1306 let mut peer_quotes = Vec::new();
1307 for _ in 0..5 {
1308 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1309 let pub_key_bytes = public_key.as_bytes().to_vec();
1310 let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1311
1312 let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1313 quote.pub_key = pub_key_bytes;
1314
1315 peer_quotes.push((encoded, quote));
1316 }
1317
1318 let proof_bytes = serialize_proof(peer_quotes);
1319 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1320
1321 assert!(result.is_err(), "Should reject payment not addressed to us");
1322 let err_msg = format!("{}", result.expect_err("should fail"));
1323 assert!(
1324 err_msg.contains("does not include this node as a recipient"),
1325 "Error should mention recipient rejection: {err_msg}"
1326 );
1327 }
1328
1329 #[tokio::test]
1330 async fn test_wrong_peer_binding_rejected() {
1331 use ant_evm::{EncodedPeerId, RewardsAddress};
1332 use saorsa_core::MlDsa65;
1333 use saorsa_pqc::pqc::MlDsaOperations;
1334
1335 let verifier = create_test_verifier();
1336 let xorname = [0xFFu8; 32];
1337 let rewards_addr = RewardsAddress::new([1u8; 20]);
1338
1339 let ml_dsa = MlDsa65::new();
1341 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1342 let pub_key_bytes = public_key.as_bytes().to_vec();
1343
1344 let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1347 quote.pub_key = pub_key_bytes;
1348
1349 let mut peer_quotes = Vec::new();
1351 for _ in 0..5 {
1352 let keypair = libp2p::identity::Keypair::generate_ed25519();
1353 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1354 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1355 }
1356
1357 let proof_bytes = serialize_proof(peer_quotes);
1358 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1359
1360 assert!(result.is_err(), "Should reject wrong peer binding");
1361 let err_msg = format!("{}", result.expect_err("should fail"));
1362 assert!(
1363 err_msg.contains("pub_key does not belong to claimed peer"),
1364 "Error should mention binding mismatch: {err_msg}"
1365 );
1366 }
1367
1368 #[tokio::test]
1373 async fn test_merkle_tagged_proof_invalid_data_rejected() {
1374 use crate::ant_protocol::PROOF_TAG_MERKLE;
1375
1376 let verifier = create_test_verifier();
1377 let xorname = [0xA1u8; 32];
1378
1379 let mut merkle_garbage = Vec::with_capacity(64);
1382 merkle_garbage.push(PROOF_TAG_MERKLE);
1383 merkle_garbage.extend_from_slice(&[0xAB; 63]);
1384
1385 let result = verifier
1386 .verify_payment(&xorname, Some(&merkle_garbage))
1387 .await;
1388
1389 assert!(
1390 result.is_err(),
1391 "Should reject merkle proof with invalid body"
1392 );
1393 let err_msg = format!("{}", result.expect_err("should fail"));
1394 assert!(
1395 err_msg.contains("deserialize") || err_msg.contains("merkle proof"),
1396 "Error should mention deserialization failure: {err_msg}"
1397 );
1398 }
1399
1400 #[tokio::test]
1401 async fn test_single_node_tagged_proof_deserialization() {
1402 use crate::payment::proof::serialize_single_node_proof;
1403 use ant_evm::{EncodedPeerId, RewardsAddress};
1404
1405 let verifier = create_test_verifier();
1406 let xorname = [0xA2u8; 32];
1407 let rewards_addr = RewardsAddress::new([1u8; 20]);
1408
1409 let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1411 let mut peer_quotes = Vec::new();
1412 for _ in 0..5 {
1413 let keypair = libp2p::identity::Keypair::generate_ed25519();
1414 let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1415 peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1416 }
1417
1418 let proof = crate::payment::proof::PaymentProof {
1419 proof_of_payment: ProofOfPayment {
1420 peer_quotes: peer_quotes.clone(),
1421 },
1422 tx_hashes: vec![],
1423 };
1424
1425 let tagged_bytes = serialize_single_node_proof(&proof).expect("serialize tagged proof");
1426
1427 assert_eq!(
1429 crate::payment::proof::detect_proof_type(&tagged_bytes),
1430 Some(crate::payment::proof::ProofType::SingleNode)
1431 );
1432
1433 let result = verifier.verify_payment(&xorname, Some(&tagged_bytes)).await;
1437
1438 assert!(result.is_err(), "Should fail at quote validation stage");
1439 let err_msg = format!("{}", result.expect_err("should fail"));
1440 assert!(
1442 !err_msg.contains("deserialize"),
1443 "Should pass deserialization but fail later: {err_msg}"
1444 );
1445 }
1446
1447 #[test]
1448 fn test_pool_cache_insert_and_lookup() {
1449 use evmlib::merkle_batch_payment::PoolHash;
1450
1451 let verifier = create_test_verifier();
1454
1455 let pool_hash: PoolHash = [0xBBu8; 32];
1456 let payment_info = ant_evm::merkle_payments::OnChainPaymentInfo {
1457 depth: 4,
1458 merkle_payment_timestamp: 1_700_000_000,
1459 paid_node_addresses: vec![],
1460 };
1461
1462 {
1464 let mut cache = verifier.pool_cache.lock();
1465 cache.put(pool_hash, payment_info);
1466 }
1467
1468 {
1470 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1471 assert!(found.is_some(), "Pool hash should be in cache after insert");
1472 let info = found.expect("cached info");
1473 assert_eq!(info.depth, 4);
1474 assert_eq!(info.merkle_payment_timestamp, 1_700_000_000);
1475 }
1476
1477 {
1479 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1480 assert!(
1481 found.is_some(),
1482 "Pool hash should still be in cache on second lookup"
1483 );
1484 }
1485
1486 let other_hash: PoolHash = [0xCCu8; 32];
1488 {
1489 let found = verifier.pool_cache.lock().get(&other_hash).cloned();
1490 assert!(found.is_none(), "Unknown pool hash should not be in cache");
1491 }
1492 }
1493}