1use crate::ant_protocol::CLOSE_GROUP_SIZE;
7use crate::error::{Error, Result};
8use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
9use crate::payment::proof::{
10 deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
11};
12use crate::payment::quote::{verify_quote_content, verify_quote_signature};
13use evmlib::contract::merkle_payment_vault;
14use evmlib::merkle_batch_payment::PoolHash;
15use evmlib::merkle_payments::OnChainPaymentInfo;
16use evmlib::Network as EvmNetwork;
17use evmlib::ProofOfPayment;
18use evmlib::RewardsAddress;
19use lru::LruCache;
20use parking_lot::Mutex;
21use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
22use std::num::NonZeroUsize;
23use std::time::SystemTime;
24use tracing::{debug, info};
25
26pub const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
31
32pub const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 262_144;
39
40const QUOTE_MAX_AGE_SECS: u64 = 86_400;
43
44const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
47
48#[derive(Debug, Clone)]
53pub struct EvmVerifierConfig {
54 pub network: EvmNetwork,
56}
57
58impl Default for EvmVerifierConfig {
59 fn default() -> Self {
60 Self {
61 network: EvmNetwork::ArbitrumOne,
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
71pub struct PaymentVerifierConfig {
72 pub evm: EvmVerifierConfig,
74 pub cache_capacity: usize,
76 pub local_rewards_address: RewardsAddress,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum PaymentStatus {
84 CachedAsVerified,
86 PaymentRequired,
88 PaymentVerified,
90}
91
92impl PaymentStatus {
93 #[must_use]
95 pub fn can_store(&self) -> bool {
96 matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
97 }
98
99 #[must_use]
101 pub fn is_cached(&self) -> bool {
102 matches!(self, Self::CachedAsVerified)
103 }
104}
105
106const DEFAULT_POOL_CACHE_CAPACITY: usize = 1_000;
108
109pub struct PaymentVerifier {
116 cache: VerifiedCache,
118 pool_cache: Mutex<LruCache<PoolHash, OnChainPaymentInfo>>,
120 config: PaymentVerifierConfig,
122}
123
124impl PaymentVerifier {
125 #[must_use]
127 pub fn new(config: PaymentVerifierConfig) -> Self {
128 const _: () = assert!(
129 DEFAULT_POOL_CACHE_CAPACITY > 0,
130 "pool cache capacity must be > 0"
131 );
132 let cache = VerifiedCache::with_capacity(config.cache_capacity);
133 let pool_cache_size =
134 NonZeroUsize::new(DEFAULT_POOL_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN);
135 let pool_cache = Mutex::new(LruCache::new(pool_cache_size));
136
137 let cache_capacity = config.cache_capacity;
138 info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on, pool_cache={DEFAULT_POOL_CACHE_CAPACITY})");
139
140 Self {
141 cache,
142 pool_cache,
143 config,
144 }
145 }
146
147 pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
162 if self.cache.contains(xorname) {
164 if tracing::enabled!(tracing::Level::DEBUG) {
165 debug!("Data {} found in verified cache", hex::encode(xorname));
166 }
167 return PaymentStatus::CachedAsVerified;
168 }
169
170 if tracing::enabled!(tracing::Level::DEBUG) {
172 debug!(
173 "Data {} not in cache - payment required",
174 hex::encode(xorname)
175 );
176 }
177 PaymentStatus::PaymentRequired
178 }
179
180 pub async fn verify_payment(
200 &self,
201 xorname: &XorName,
202 payment_proof: Option<&[u8]>,
203 ) -> Result<PaymentStatus> {
204 let status = self.check_payment_required(xorname);
206
207 match status {
208 PaymentStatus::CachedAsVerified => {
209 Ok(status)
211 }
212 PaymentStatus::PaymentRequired => {
213 if let Some(proof) = payment_proof {
215 let proof_len = proof.len();
216 if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
217 return Err(Error::Payment(format!(
218 "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
219 )));
220 }
221 if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
222 return Err(Error::Payment(format!(
223 "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
224 )));
225 }
226
227 match detect_proof_type(proof) {
229 Some(ProofType::Merkle) => {
230 self.verify_merkle_payment(xorname, proof).await?;
231 }
232 Some(ProofType::SingleNode) => {
233 let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
234 Error::Payment(format!("Failed to deserialize payment proof: {e}"))
235 })?;
236
237 if !tx_hashes.is_empty() {
238 debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
239 }
240
241 self.verify_evm_payment(xorname, &payment).await?;
242 }
243 None => {
244 let tag = proof.first().copied().unwrap_or(0);
245 return Err(Error::Payment(format!(
246 "Unknown payment proof type tag: 0x{tag:02x}"
247 )));
248 }
249 }
250
251 self.cache.insert(*xorname);
253
254 Ok(PaymentStatus::PaymentVerified)
255 } else {
256 let xorname_hex = hex::encode(xorname);
258 Err(Error::Payment(format!(
259 "Payment required for new data {xorname_hex}"
260 )))
261 }
262 }
263 PaymentStatus::PaymentVerified => Err(Error::Payment(
264 "Unexpected PaymentVerified status from check_payment_required".to_string(),
265 )),
266 }
267 }
268
269 #[must_use]
271 pub fn cache_stats(&self) -> CacheStats {
272 self.cache.stats()
273 }
274
275 #[must_use]
277 pub fn cache_len(&self) -> usize {
278 self.cache.len()
279 }
280
281 #[cfg(any(test, feature = "test-utils"))]
287 pub fn cache_insert(&self, xorname: XorName) {
288 self.cache.insert(xorname);
289 }
290
291 async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
305 if tracing::enabled!(tracing::Level::DEBUG) {
306 let xorname_hex = hex::encode(xorname);
307 let quote_count = payment.peer_quotes.len();
308 debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
309 }
310
311 Self::validate_quote_structure(payment)?;
312 Self::validate_quote_content(payment, xorname)?;
313 Self::validate_quote_timestamps(payment)?;
314 Self::validate_peer_bindings(payment)?;
315 self.validate_local_recipient(payment)?;
316
317 let peer_quotes = payment.peer_quotes.clone();
319 tokio::task::spawn_blocking(move || {
320 for (encoded_peer_id, quote) in &peer_quotes {
321 if !verify_quote_signature(quote) {
322 return Err(Error::Payment(
323 format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
324 ));
325 }
326 }
327 Ok(())
328 })
329 .await
330 .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
331
332 let payment_digest = payment.digest();
348 if payment_digest.is_empty() {
349 return Err(Error::Payment("Payment has no quotes".to_string()));
350 }
351
352 let payment_verifications: Vec<_> = payment_digest
353 .into_iter()
354 .map(
355 evmlib::contract::payment_vault::interface::IPaymentVault::PaymentVerification::from,
356 )
357 .collect();
358
359 let provider = evmlib::utils::http_provider(self.config.evm.network.rpc_url().clone());
360 let handler = evmlib::contract::payment_vault::handler::PaymentVaultHandler::new(
361 *self.config.evm.network.data_payments_address(),
362 provider,
363 );
364
365 let results = handler
366 .verify_payment(payment_verifications)
367 .await
368 .map_err(|e| {
369 let xorname_hex = hex::encode(xorname);
370 Error::Payment(format!("EVM verification error for {xorname_hex}: {e}"))
371 })?;
372
373 let paid_results: Vec<_> = results
374 .iter()
375 .filter(|r| r.amountPaid > evmlib::common::U256::ZERO)
376 .collect();
377
378 if paid_results.is_empty() {
379 let xorname_hex = hex::encode(xorname);
380 return Err(Error::Payment(format!(
381 "Payment verification failed on-chain for {xorname_hex} (no paid quotes found)"
382 )));
383 }
384
385 for result in &paid_results {
386 if !result.isValid {
387 let xorname_hex = hex::encode(xorname);
388 return Err(Error::Payment(format!(
389 "Payment verification failed on-chain for {xorname_hex} (paid quote is invalid)"
390 )));
391 }
392 }
393
394 if tracing::enabled!(tracing::Level::INFO) {
395 let valid_count = paid_results.len();
396 let total_results = results.len();
397 let xorname_hex = hex::encode(xorname);
398 info!(
399 "EVM payment verified for {xorname_hex} ({valid_count} paid and valid, {total_results} total results)"
400 );
401 }
402 Ok(())
403 }
404
405 fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
407 if payment.peer_quotes.is_empty() {
408 return Err(Error::Payment("Payment has no quotes".to_string()));
409 }
410
411 let quote_count = payment.peer_quotes.len();
412 if quote_count != CLOSE_GROUP_SIZE {
413 return Err(Error::Payment(format!(
414 "Payment must have exactly {CLOSE_GROUP_SIZE} quotes, got {quote_count}"
415 )));
416 }
417
418 let mut seen: Vec<&evmlib::EncodedPeerId> = Vec::with_capacity(quote_count);
419 for (encoded_peer_id, _) in &payment.peer_quotes {
420 if seen.contains(&encoded_peer_id) {
421 return Err(Error::Payment(format!(
422 "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
423 )));
424 }
425 seen.push(encoded_peer_id);
426 }
427
428 Ok(())
429 }
430
431 fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
433 for (encoded_peer_id, quote) in &payment.peer_quotes {
434 if !verify_quote_content(quote, xorname) {
435 let expected_hex = hex::encode(xorname);
436 let actual_hex = hex::encode(quote.content.0);
437 return Err(Error::Payment(format!(
438 "Quote content address mismatch for peer {encoded_peer_id:?}: expected {expected_hex}, got {actual_hex}"
439 )));
440 }
441 }
442 Ok(())
443 }
444
445 fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
447 let now = SystemTime::now();
448 for (encoded_peer_id, quote) in &payment.peer_quotes {
449 match now.duration_since(quote.timestamp) {
450 Ok(age) => {
451 if age.as_secs() > QUOTE_MAX_AGE_SECS {
452 return Err(Error::Payment(format!(
453 "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
454 age.as_secs()
455 )));
456 }
457 }
458 Err(_) => {
459 if let Ok(skew) = quote.timestamp.duration_since(now) {
460 if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
461 return Err(Error::Payment(format!(
462 "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
463 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
464 skew.as_secs()
465 )));
466 }
467 } else {
468 return Err(Error::Payment(format!(
469 "Quote from peer {encoded_peer_id:?} has invalid timestamp"
470 )));
471 }
472 }
473 }
474 }
475 Ok(())
476 }
477
478 fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
480 for (encoded_peer_id, quote) in &payment.peer_quotes {
481 let expected_peer_id = peer_id_from_public_key_bytes("e.pub_key)
482 .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
483
484 if expected_peer_id.as_bytes() != encoded_peer_id.as_bytes() {
485 let expected_hex = expected_peer_id.to_hex();
486 let actual_hex = hex::encode(encoded_peer_id.as_bytes());
487 return Err(Error::Payment(format!(
488 "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
489 BLAKE3(pub_key) = {expected_hex}, peer_id = {actual_hex}"
490 )));
491 }
492 }
493 Ok(())
494 }
495
496 #[allow(clippy::too_many_lines)]
505 async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> {
506 if tracing::enabled!(tracing::Level::DEBUG) {
507 debug!("Verifying merkle payment for {}", hex::encode(xorname));
508 }
509
510 let merkle_proof = deserialize_merkle_proof(proof_bytes)
512 .map_err(|e| Error::Payment(format!("Failed to deserialize merkle proof: {e}")))?;
513
514 if merkle_proof.address.0 != *xorname {
516 let proof_hex = hex::encode(merkle_proof.address.0);
517 let store_hex = hex::encode(xorname);
518 return Err(Error::Payment(format!(
519 "Merkle proof address mismatch: proof is for {proof_hex}, but storing {store_hex}"
520 )));
521 }
522
523 let pool_hash = merkle_proof.winner_pool_hash();
524
525 let cached_info = {
527 let mut pool_cache = self.pool_cache.lock();
528 pool_cache.get(&pool_hash).cloned()
529 };
530
531 let payment_info = if let Some(info) = cached_info {
532 debug!("Pool cache hit for hash {}", hex::encode(pool_hash));
533 info
534 } else {
535 let info =
537 merkle_payment_vault::get_merkle_payment_info(&self.config.evm.network, pool_hash)
538 .await
539 .map_err(|e| {
540 let pool_hex = hex::encode(pool_hash);
541 Error::Payment(format!(
542 "Failed to query merkle payment info for pool {pool_hex}: {e}"
543 ))
544 })?;
545
546 let paid_node_addresses: Vec<_> = info
547 .paidNodeAddresses
548 .iter()
549 .map(|pna| (pna.rewardsAddress, usize::from(pna.poolIndex)))
550 .collect();
551
552 let on_chain_info = OnChainPaymentInfo {
553 depth: info.depth,
554 merkle_payment_timestamp: info.merklePaymentTimestamp,
555 paid_node_addresses,
556 };
557
558 {
560 let mut pool_cache = self.pool_cache.lock();
561 pool_cache.put(pool_hash, on_chain_info.clone());
562 }
563
564 debug!(
565 "Queried on-chain merkle payment info for pool {}: depth={}, timestamp={}, paid_nodes={}",
566 hex::encode(pool_hash),
567 on_chain_info.depth,
568 on_chain_info.merkle_payment_timestamp,
569 on_chain_info.paid_node_addresses.len()
570 );
571
572 on_chain_info
573 };
574
575 for candidate in &merkle_proof.winner_pool.candidate_nodes {
583 if !crate::payment::verify_merkle_candidate_signature(candidate) {
584 return Err(Error::Payment(format!(
585 "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})",
586 candidate.reward_address
587 )));
588 }
589 if candidate.merkle_payment_timestamp != payment_info.merkle_payment_timestamp {
590 return Err(Error::Payment(format!(
591 "Candidate timestamp mismatch: expected {}, got {} (reward: {})",
592 payment_info.merkle_payment_timestamp,
593 candidate.merkle_payment_timestamp,
594 candidate.reward_address
595 )));
596 }
597 }
598
599 let smart_contract_root = merkle_proof.winner_pool.midpoint_proof.root();
601
602 evmlib::merkle_payments::verify_merkle_proof(
605 &merkle_proof.address,
606 &merkle_proof.data_proof,
607 &merkle_proof.winner_pool.midpoint_proof,
608 payment_info.depth,
609 smart_contract_root,
610 payment_info.merkle_payment_timestamp,
611 )
612 .map_err(|e| {
613 let xorname_hex = hex::encode(xorname);
614 Error::Payment(format!(
615 "Merkle proof verification failed for {xorname_hex}: {e}"
616 ))
617 })?;
618
619 let expected_depth = payment_info.depth as usize;
621 let actual_paid = payment_info.paid_node_addresses.len();
622 if actual_paid != expected_depth {
623 return Err(Error::Payment(format!(
624 "Wrong number of paid nodes: expected {expected_depth}, got {actual_paid}"
625 )));
626 }
627
628 for (addr, idx) in &payment_info.paid_node_addresses {
638 let node = merkle_proof
639 .winner_pool
640 .candidate_nodes
641 .get(*idx)
642 .ok_or_else(|| {
643 Error::Payment(format!(
644 "Paid node index {idx} out of bounds for pool size {}",
645 merkle_proof.winner_pool.candidate_nodes.len()
646 ))
647 })?;
648 if node.reward_address != *addr {
649 return Err(Error::Payment(format!(
650 "Paid node address mismatch at index {idx}: expected {addr}, got {}",
651 node.reward_address
652 )));
653 }
654 }
655
656 if tracing::enabled!(tracing::Level::INFO) {
657 info!(
658 "Merkle payment verified for {} (pool: {})",
659 hex::encode(xorname),
660 hex::encode(pool_hash)
661 );
662 }
663
664 Ok(())
665 }
666
667 fn validate_local_recipient(&self, payment: &ProofOfPayment) -> Result<()> {
669 let local_addr = &self.config.local_rewards_address;
670 let is_recipient = payment
671 .peer_quotes
672 .iter()
673 .any(|(_, quote)| quote.rewards_address == *local_addr);
674 if !is_recipient {
675 return Err(Error::Payment(
676 "Payment proof does not include this node as a recipient".to_string(),
677 ));
678 }
679 Ok(())
680 }
681}
682
683#[cfg(test)]
684#[allow(clippy::expect_used)]
685mod tests {
686 use super::*;
687
688 fn create_test_verifier() -> PaymentVerifier {
691 let config = PaymentVerifierConfig {
692 evm: EvmVerifierConfig::default(),
693 cache_capacity: 100,
694 local_rewards_address: RewardsAddress::new([1u8; 20]),
695 };
696 PaymentVerifier::new(config)
697 }
698
699 #[test]
700 fn test_payment_required_for_new_data() {
701 let verifier = create_test_verifier();
702 let xorname = [1u8; 32];
703
704 let status = verifier.check_payment_required(&xorname);
706 assert_eq!(status, PaymentStatus::PaymentRequired);
707 }
708
709 #[test]
710 fn test_cache_hit() {
711 let verifier = create_test_verifier();
712 let xorname = [1u8; 32];
713
714 verifier.cache.insert(xorname);
716
717 let status = verifier.check_payment_required(&xorname);
719 assert_eq!(status, PaymentStatus::CachedAsVerified);
720 }
721
722 #[tokio::test]
723 async fn test_verify_payment_without_proof_rejected() {
724 let verifier = create_test_verifier();
725 let xorname = [1u8; 32];
726
727 let result = verifier.verify_payment(&xorname, None).await;
729 assert!(
730 result.is_err(),
731 "Expected Err without proof, got: {result:?}"
732 );
733 }
734
735 #[tokio::test]
736 async fn test_verify_payment_cached() {
737 let verifier = create_test_verifier();
738 let xorname = [1u8; 32];
739
740 verifier.cache.insert(xorname);
742
743 let result = verifier.verify_payment(&xorname, None).await;
745 assert!(result.is_ok());
746 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
747 }
748
749 #[test]
750 fn test_payment_status_can_store() {
751 assert!(PaymentStatus::CachedAsVerified.can_store());
752 assert!(PaymentStatus::PaymentVerified.can_store());
753 assert!(!PaymentStatus::PaymentRequired.can_store());
754 }
755
756 #[test]
757 fn test_payment_status_is_cached() {
758 assert!(PaymentStatus::CachedAsVerified.is_cached());
759 assert!(!PaymentStatus::PaymentVerified.is_cached());
760 assert!(!PaymentStatus::PaymentRequired.is_cached());
761 }
762
763 #[tokio::test]
764 async fn test_cache_preload_bypasses_evm() {
765 let verifier = create_test_verifier();
766 let xorname = [42u8; 32];
767
768 assert_eq!(
770 verifier.check_payment_required(&xorname),
771 PaymentStatus::PaymentRequired
772 );
773
774 verifier.cache.insert(xorname);
776
777 assert_eq!(
779 verifier.check_payment_required(&xorname),
780 PaymentStatus::CachedAsVerified
781 );
782 }
783
784 #[tokio::test]
785 async fn test_proof_too_small() {
786 let verifier = create_test_verifier();
787 let xorname = [1u8; 32];
788
789 let small_proof = vec![0u8; MIN_PAYMENT_PROOF_SIZE_BYTES - 1];
791 let result = verifier.verify_payment(&xorname, Some(&small_proof)).await;
792 assert!(result.is_err());
793 let err_msg = format!("{}", result.expect_err("should fail"));
794 assert!(
795 err_msg.contains("too small"),
796 "Error should mention 'too small': {err_msg}"
797 );
798 }
799
800 #[tokio::test]
801 async fn test_proof_too_large() {
802 let verifier = create_test_verifier();
803 let xorname = [2u8; 32];
804
805 let large_proof = vec![0u8; MAX_PAYMENT_PROOF_SIZE_BYTES + 1];
807 let result = verifier.verify_payment(&xorname, Some(&large_proof)).await;
808 assert!(result.is_err());
809 let err_msg = format!("{}", result.expect_err("should fail"));
810 assert!(
811 err_msg.contains("too large"),
812 "Error should mention 'too large': {err_msg}"
813 );
814 }
815
816 #[tokio::test]
817 async fn test_proof_at_min_boundary_unknown_tag() {
818 let verifier = create_test_verifier();
819 let xorname = [3u8; 32];
820
821 let boundary_proof = vec![0xFFu8; MIN_PAYMENT_PROOF_SIZE_BYTES];
823 let result = verifier
824 .verify_payment(&xorname, Some(&boundary_proof))
825 .await;
826 assert!(result.is_err());
827 let err_msg = format!("{}", result.expect_err("should fail"));
828 assert!(
829 err_msg.contains("Unknown payment proof type tag"),
830 "Error should mention unknown tag: {err_msg}"
831 );
832 }
833
834 #[tokio::test]
835 async fn test_proof_at_max_boundary_unknown_tag() {
836 let verifier = create_test_verifier();
837 let xorname = [4u8; 32];
838
839 let boundary_proof = vec![0xFFu8; MAX_PAYMENT_PROOF_SIZE_BYTES];
841 let result = verifier
842 .verify_payment(&xorname, Some(&boundary_proof))
843 .await;
844 assert!(result.is_err());
845 let err_msg = format!("{}", result.expect_err("should fail"));
846 assert!(
847 err_msg.contains("Unknown payment proof type tag"),
848 "Error should mention unknown tag: {err_msg}"
849 );
850 }
851
852 #[tokio::test]
853 async fn test_malformed_single_node_proof() {
854 let verifier = create_test_verifier();
855 let xorname = [5u8; 32];
856
857 let mut garbage = vec![crate::ant_protocol::PROOF_TAG_SINGLE_NODE];
859 garbage.extend_from_slice(&[0xAB; 63]);
860 let result = verifier.verify_payment(&xorname, Some(&garbage)).await;
861 assert!(result.is_err());
862 let err_msg = format!("{}", result.expect_err("should fail"));
863 assert!(
864 err_msg.contains("deserialize") || err_msg.contains("Failed"),
865 "Error should mention deserialization failure: {err_msg}"
866 );
867 }
868
869 #[test]
870 fn test_cache_len_getter() {
871 let verifier = create_test_verifier();
872 assert_eq!(verifier.cache_len(), 0);
873
874 verifier.cache.insert([10u8; 32]);
875 assert_eq!(verifier.cache_len(), 1);
876
877 verifier.cache.insert([20u8; 32]);
878 assert_eq!(verifier.cache_len(), 2);
879 }
880
881 #[test]
882 fn test_cache_stats_after_operations() {
883 let verifier = create_test_verifier();
884 let xorname = [7u8; 32];
885
886 verifier.check_payment_required(&xorname);
888 let stats = verifier.cache_stats();
889 assert_eq!(stats.misses, 1);
890 assert_eq!(stats.hits, 0);
891
892 verifier.cache.insert(xorname);
894 verifier.check_payment_required(&xorname);
895 let stats = verifier.cache_stats();
896 assert_eq!(stats.hits, 1);
897 assert_eq!(stats.misses, 1);
898 assert_eq!(stats.additions, 1);
899 }
900
901 #[tokio::test]
902 async fn test_concurrent_cache_lookups() {
903 let verifier = std::sync::Arc::new(create_test_verifier());
904
905 for i in 0..10u8 {
907 verifier.cache.insert([i; 32]);
908 }
909
910 let mut handles = Vec::new();
911 for i in 0..10u8 {
912 let v = verifier.clone();
913 handles.push(tokio::spawn(async move {
914 let xorname = [i; 32];
915 v.verify_payment(&xorname, None).await
916 }));
917 }
918
919 for handle in handles {
920 let result = handle.await.expect("task panicked");
921 assert!(result.is_ok());
922 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
923 }
924
925 assert_eq!(verifier.cache_len(), 10);
926 }
927
928 #[test]
929 fn test_default_evm_config() {
930 let _config = EvmVerifierConfig::default();
931 }
933
934 #[test]
935 fn test_real_ml_dsa_proof_size_within_limits() {
936 use crate::payment::metrics::QuotingMetricsTracker;
937 use crate::payment::proof::PaymentProof;
938 use crate::payment::quote::{QuoteGenerator, XorName};
939 use alloy::primitives::FixedBytes;
940 use evmlib::{EncodedPeerId, RewardsAddress};
941 use saorsa_core::MlDsa65;
942 use saorsa_pqc::pqc::types::MlDsaSecretKey;
943 use saorsa_pqc::pqc::MlDsaOperations;
944
945 let ml_dsa = MlDsa65::new();
946 let mut peer_quotes = Vec::new();
947
948 for i in 0..5u8 {
949 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
950
951 let rewards_address = RewardsAddress::new([i; 20]);
952 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
953 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
954
955 let pub_key_bytes = public_key.as_bytes().to_vec();
956 let sk_bytes = secret_key.as_bytes().to_vec();
957 generator.set_signer(pub_key_bytes, move |msg| {
958 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
959 let ml_dsa = MlDsa65::new();
960 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
961 });
962
963 let content: XorName = [i; 32];
964 let quote = generator.create_quote(content, 4096, 0).expect("quote");
965
966 peer_quotes.push((EncodedPeerId::new(rand::random()), quote));
967 }
968
969 let proof = PaymentProof {
970 proof_of_payment: ProofOfPayment { peer_quotes },
971 tx_hashes: vec![FixedBytes::from([0xABu8; 32])],
972 };
973
974 let proof_bytes =
975 crate::payment::proof::serialize_single_node_proof(&proof).expect("serialize");
976
977 assert!(
980 proof_bytes.len() > 20_000,
981 "Real 5-quote ML-DSA proof should be > 20 KB, got {} bytes",
982 proof_bytes.len()
983 );
984 assert!(
985 proof_bytes.len() < MAX_PAYMENT_PROOF_SIZE_BYTES,
986 "Real 5-quote ML-DSA proof ({} bytes) should fit within {} byte limit",
987 proof_bytes.len(),
988 MAX_PAYMENT_PROOF_SIZE_BYTES
989 );
990 }
991
992 #[tokio::test]
993 async fn test_content_address_mismatch_rejected() {
994 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
995 use evmlib::quoting_metrics::QuotingMetrics;
996 use evmlib::{EncodedPeerId, PaymentQuote, RewardsAddress};
997 use std::time::SystemTime;
998
999 let verifier = create_test_verifier();
1000
1001 let target_xorname = [0xAAu8; 32];
1003
1004 let wrong_xorname = [0xBBu8; 32];
1006 let quote = PaymentQuote {
1007 content: xor_name::XorName(wrong_xorname),
1008 timestamp: SystemTime::now(),
1009 quoting_metrics: QuotingMetrics {
1010 data_size: 1024,
1011 data_type: 0,
1012 close_records_stored: 0,
1013 records_per_type: vec![],
1014 max_records: 1000,
1015 received_payment_count: 0,
1016 live_time: 0,
1017 network_density: None,
1018 network_size: None,
1019 },
1020 rewards_address: RewardsAddress::new([1u8; 20]),
1021 pub_key: vec![0u8; 64],
1022 signature: vec![0u8; 64],
1023 };
1024
1025 let mut peer_quotes = Vec::new();
1027 for _ in 0..CLOSE_GROUP_SIZE {
1028 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1029 }
1030
1031 let proof = PaymentProof {
1032 proof_of_payment: ProofOfPayment { peer_quotes },
1033 tx_hashes: vec![],
1034 };
1035
1036 let proof_bytes = serialize_single_node_proof(&proof).expect("serialize proof");
1037
1038 let result = verifier
1039 .verify_payment(&target_xorname, Some(&proof_bytes))
1040 .await;
1041
1042 assert!(result.is_err(), "Should reject mismatched content address");
1043 let err_msg = format!("{}", result.expect_err("should be error"));
1044 assert!(
1045 err_msg.contains("content address mismatch"),
1046 "Error should mention 'content address mismatch': {err_msg}"
1047 );
1048 }
1049
1050 fn make_fake_quote(
1052 xorname: [u8; 32],
1053 timestamp: SystemTime,
1054 rewards_address: RewardsAddress,
1055 ) -> evmlib::PaymentQuote {
1056 use evmlib::quoting_metrics::QuotingMetrics;
1057 use evmlib::PaymentQuote;
1058
1059 PaymentQuote {
1060 content: xor_name::XorName(xorname),
1061 timestamp,
1062 quoting_metrics: QuotingMetrics {
1063 data_size: 1024,
1064 data_type: 0,
1065 close_records_stored: 0,
1066 records_per_type: vec![],
1067 max_records: 1000,
1068 received_payment_count: 0,
1069 live_time: 0,
1070 network_density: None,
1071 network_size: None,
1072 },
1073 rewards_address,
1074 pub_key: vec![0u8; 64],
1075 signature: vec![0u8; 64],
1076 }
1077 }
1078
1079 fn serialize_proof(peer_quotes: Vec<(evmlib::EncodedPeerId, evmlib::PaymentQuote)>) -> Vec<u8> {
1081 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1082
1083 let proof = PaymentProof {
1084 proof_of_payment: ProofOfPayment { peer_quotes },
1085 tx_hashes: vec![],
1086 };
1087 serialize_single_node_proof(&proof).expect("serialize proof")
1088 }
1089
1090 #[tokio::test]
1091 async fn test_expired_quote_rejected() {
1092 use evmlib::{EncodedPeerId, RewardsAddress};
1093 use std::time::Duration;
1094
1095 let verifier = create_test_verifier();
1096 let xorname = [0xCCu8; 32];
1097 let rewards_addr = RewardsAddress::new([1u8; 20]);
1098
1099 let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
1101 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1102
1103 let mut peer_quotes = Vec::new();
1104 for _ in 0..CLOSE_GROUP_SIZE {
1105 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1106 }
1107
1108 let proof_bytes = serialize_proof(peer_quotes);
1109 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1110
1111 assert!(result.is_err(), "Should reject expired quote");
1112 let err_msg = format!("{}", result.expect_err("should fail"));
1113 assert!(
1114 err_msg.contains("expired"),
1115 "Error should mention 'expired': {err_msg}"
1116 );
1117 }
1118
1119 #[tokio::test]
1120 async fn test_future_timestamp_rejected() {
1121 use evmlib::{EncodedPeerId, RewardsAddress};
1122 use std::time::Duration;
1123
1124 let verifier = create_test_verifier();
1125 let xorname = [0xDDu8; 32];
1126 let rewards_addr = RewardsAddress::new([1u8; 20]);
1127
1128 let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
1130 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1131
1132 let mut peer_quotes = Vec::new();
1133 for _ in 0..CLOSE_GROUP_SIZE {
1134 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1135 }
1136
1137 let proof_bytes = serialize_proof(peer_quotes);
1138 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1139
1140 assert!(result.is_err(), "Should reject future-timestamped quote");
1141 let err_msg = format!("{}", result.expect_err("should fail"));
1142 assert!(
1143 err_msg.contains("future"),
1144 "Error should mention 'future': {err_msg}"
1145 );
1146 }
1147
1148 #[tokio::test]
1149 async fn test_quote_within_clock_skew_tolerance_accepted() {
1150 use evmlib::{EncodedPeerId, RewardsAddress};
1151 use std::time::Duration;
1152
1153 let verifier = create_test_verifier();
1154 let xorname = [0xD1u8; 32];
1155 let rewards_addr = RewardsAddress::new([1u8; 20]);
1156
1157 let future_timestamp = SystemTime::now() + Duration::from_secs(30);
1159 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1160
1161 let mut peer_quotes = Vec::new();
1162 for _ in 0..CLOSE_GROUP_SIZE {
1163 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1164 }
1165
1166 let proof_bytes = serialize_proof(peer_quotes);
1167 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1168
1169 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1171 assert!(
1172 !err_msg.contains("future"),
1173 "Should pass timestamp check (within tolerance), but got: {err_msg}"
1174 );
1175 }
1176
1177 #[tokio::test]
1178 async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
1179 use evmlib::{EncodedPeerId, RewardsAddress};
1180 use std::time::Duration;
1181
1182 let verifier = create_test_verifier();
1183 let xorname = [0xD2u8; 32];
1184 let rewards_addr = RewardsAddress::new([1u8; 20]);
1185
1186 let future_timestamp = SystemTime::now() + Duration::from_secs(120);
1188 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1189
1190 let mut peer_quotes = Vec::new();
1191 for _ in 0..CLOSE_GROUP_SIZE {
1192 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1193 }
1194
1195 let proof_bytes = serialize_proof(peer_quotes);
1196 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1197
1198 assert!(
1199 result.is_err(),
1200 "Should reject quote beyond clock skew tolerance"
1201 );
1202 let err_msg = format!("{}", result.expect_err("should fail"));
1203 assert!(
1204 err_msg.contains("future"),
1205 "Error should mention 'future': {err_msg}"
1206 );
1207 }
1208
1209 #[tokio::test]
1210 async fn test_quote_23h_old_still_accepted() {
1211 use evmlib::{EncodedPeerId, RewardsAddress};
1212 use std::time::Duration;
1213
1214 let verifier = create_test_verifier();
1215 let xorname = [0xD3u8; 32];
1216 let rewards_addr = RewardsAddress::new([1u8; 20]);
1217
1218 let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
1220 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1221
1222 let mut peer_quotes = Vec::new();
1223 for _ in 0..CLOSE_GROUP_SIZE {
1224 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1225 }
1226
1227 let proof_bytes = serialize_proof(peer_quotes);
1228 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1229
1230 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1232 assert!(
1233 !err_msg.contains("expired"),
1234 "Should pass expiry check (23h < 24h), but got: {err_msg}"
1235 );
1236 }
1237
1238 fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> evmlib::EncodedPeerId {
1240 let ant_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1241 evmlib::EncodedPeerId::new(*ant_peer_id.as_bytes())
1242 }
1243
1244 #[tokio::test]
1245 async fn test_local_not_in_paid_set_rejected() {
1246 use evmlib::RewardsAddress;
1247 use saorsa_core::MlDsa65;
1248 use saorsa_pqc::pqc::MlDsaOperations;
1249
1250 let local_addr = RewardsAddress::new([0xAAu8; 20]);
1252 let config = PaymentVerifierConfig {
1253 evm: EvmVerifierConfig {
1254 network: EvmNetwork::ArbitrumOne,
1255 },
1256 cache_capacity: 100,
1257 local_rewards_address: local_addr,
1258 };
1259 let verifier = PaymentVerifier::new(config);
1260
1261 let xorname = [0xEEu8; 32];
1262 let other_addr = RewardsAddress::new([0xBBu8; 20]);
1264
1265 let ml_dsa = MlDsa65::new();
1267 let mut peer_quotes = Vec::new();
1268 for _ in 0..CLOSE_GROUP_SIZE {
1269 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1270 let pub_key_bytes = public_key.as_bytes().to_vec();
1271 let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1272
1273 let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1274 quote.pub_key = pub_key_bytes;
1275
1276 peer_quotes.push((encoded, quote));
1277 }
1278
1279 let proof_bytes = serialize_proof(peer_quotes);
1280 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1281
1282 assert!(result.is_err(), "Should reject payment not addressed to us");
1283 let err_msg = format!("{}", result.expect_err("should fail"));
1284 assert!(
1285 err_msg.contains("does not include this node as a recipient"),
1286 "Error should mention recipient rejection: {err_msg}"
1287 );
1288 }
1289
1290 #[tokio::test]
1291 async fn test_wrong_peer_binding_rejected() {
1292 use evmlib::{EncodedPeerId, RewardsAddress};
1293 use saorsa_core::MlDsa65;
1294 use saorsa_pqc::pqc::MlDsaOperations;
1295
1296 let verifier = create_test_verifier();
1297 let xorname = [0xFFu8; 32];
1298 let rewards_addr = RewardsAddress::new([1u8; 20]);
1299
1300 let ml_dsa = MlDsa65::new();
1302 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1303 let pub_key_bytes = public_key.as_bytes().to_vec();
1304
1305 let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1308 quote.pub_key = pub_key_bytes;
1309
1310 let mut peer_quotes = Vec::new();
1312 for _ in 0..CLOSE_GROUP_SIZE {
1313 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1314 }
1315
1316 let proof_bytes = serialize_proof(peer_quotes);
1317 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1318
1319 assert!(result.is_err(), "Should reject wrong peer binding");
1320 let err_msg = format!("{}", result.expect_err("should fail"));
1321 assert!(
1322 err_msg.contains("pub_key does not belong to claimed peer"),
1323 "Error should mention binding mismatch: {err_msg}"
1324 );
1325 }
1326
1327 #[tokio::test]
1332 async fn test_merkle_tagged_proof_invalid_data_rejected() {
1333 use crate::ant_protocol::PROOF_TAG_MERKLE;
1334
1335 let verifier = create_test_verifier();
1336 let xorname = [0xA1u8; 32];
1337
1338 let mut merkle_garbage = Vec::with_capacity(64);
1341 merkle_garbage.push(PROOF_TAG_MERKLE);
1342 merkle_garbage.extend_from_slice(&[0xAB; 63]);
1343
1344 let result = verifier
1345 .verify_payment(&xorname, Some(&merkle_garbage))
1346 .await;
1347
1348 assert!(
1349 result.is_err(),
1350 "Should reject merkle proof with invalid body"
1351 );
1352 let err_msg = format!("{}", result.expect_err("should fail"));
1353 assert!(
1354 err_msg.contains("deserialize") || err_msg.contains("merkle proof"),
1355 "Error should mention deserialization failure: {err_msg}"
1356 );
1357 }
1358
1359 #[tokio::test]
1360 async fn test_single_node_tagged_proof_deserialization() {
1361 use crate::payment::proof::serialize_single_node_proof;
1362 use evmlib::{EncodedPeerId, RewardsAddress};
1363
1364 let verifier = create_test_verifier();
1365 let xorname = [0xA2u8; 32];
1366 let rewards_addr = RewardsAddress::new([1u8; 20]);
1367
1368 let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1370 let mut peer_quotes = Vec::new();
1371 for _ in 0..CLOSE_GROUP_SIZE {
1372 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1373 }
1374
1375 let proof = crate::payment::proof::PaymentProof {
1376 proof_of_payment: ProofOfPayment {
1377 peer_quotes: peer_quotes.clone(),
1378 },
1379 tx_hashes: vec![],
1380 };
1381
1382 let tagged_bytes = serialize_single_node_proof(&proof).expect("serialize tagged proof");
1383
1384 assert_eq!(
1386 crate::payment::proof::detect_proof_type(&tagged_bytes),
1387 Some(crate::payment::proof::ProofType::SingleNode)
1388 );
1389
1390 let result = verifier.verify_payment(&xorname, Some(&tagged_bytes)).await;
1394
1395 assert!(result.is_err(), "Should fail at quote validation stage");
1396 let err_msg = format!("{}", result.expect_err("should fail"));
1397 assert!(
1399 !err_msg.contains("deserialize"),
1400 "Should pass deserialization but fail later: {err_msg}"
1401 );
1402 }
1403
1404 #[test]
1405 fn test_pool_cache_insert_and_lookup() {
1406 use evmlib::merkle_batch_payment::PoolHash;
1407
1408 let verifier = create_test_verifier();
1411
1412 let pool_hash: PoolHash = [0xBBu8; 32];
1413 let payment_info = evmlib::merkle_payments::OnChainPaymentInfo {
1414 depth: 4,
1415 merkle_payment_timestamp: 1_700_000_000,
1416 paid_node_addresses: vec![],
1417 };
1418
1419 {
1421 let mut cache = verifier.pool_cache.lock();
1422 cache.put(pool_hash, payment_info);
1423 }
1424
1425 {
1427 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1428 assert!(found.is_some(), "Pool hash should be in cache after insert");
1429 let info = found.expect("cached info");
1430 assert_eq!(info.depth, 4);
1431 assert_eq!(info.merkle_payment_timestamp, 1_700_000_000);
1432 }
1433
1434 {
1436 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1437 assert!(
1438 found.is_some(),
1439 "Pool hash should still be in cache on second lookup"
1440 );
1441 }
1442
1443 let other_hash: PoolHash = [0xCCu8; 32];
1445 {
1446 let found = verifier.pool_cache.lock().get(&other_hash).cloned();
1447 assert!(found.is_none(), "Unknown pool hash should not be in cache");
1448 }
1449 }
1450
1451 fn make_candidate_nodes(
1457 timestamp: u64,
1458 ) -> [evmlib::merkle_payments::MerklePaymentCandidateNode;
1459 evmlib::merkle_payments::CANDIDATES_PER_POOL] {
1460 use evmlib::merkle_payments::{MerklePaymentCandidateNode, CANDIDATES_PER_POOL};
1461 use saorsa_core::MlDsa65;
1462 use saorsa_pqc::pqc::types::MlDsaSecretKey;
1463 use saorsa_pqc::pqc::MlDsaOperations;
1464
1465 std::array::from_fn::<_, CANDIDATES_PER_POOL, _>(|i| {
1466 let ml_dsa = MlDsa65::new();
1467 let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
1468 let metrics = evmlib::quoting_metrics::QuotingMetrics {
1469 data_size: 1024,
1470 data_type: 0,
1471 close_records_stored: i * 10,
1472 records_per_type: vec![],
1473 max_records: 500,
1474 received_payment_count: 0,
1475 live_time: 100,
1476 network_density: None,
1477 network_size: None,
1478 };
1479 #[allow(clippy::cast_possible_truncation)]
1480 let reward_address = RewardsAddress::new([i as u8; 20]);
1481 let msg =
1482 MerklePaymentCandidateNode::bytes_to_sign(&metrics, &reward_address, timestamp);
1483 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
1484 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
1485
1486 MerklePaymentCandidateNode {
1487 pub_key: pub_key.as_bytes().to_vec(),
1488 quoting_metrics: metrics,
1489 reward_address,
1490 merkle_payment_timestamp: timestamp,
1491 signature,
1492 }
1493 })
1494 }
1495
1496 fn make_valid_merkle_proof() -> (
1499 evmlib::merkle_payments::MerklePaymentProof,
1500 evmlib::merkle_batch_payment::PoolHash,
1501 [u8; 32],
1502 u64,
1503 ) {
1504 use evmlib::merkle_payments::{MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree};
1505
1506 let timestamp = std::time::SystemTime::now()
1507 .duration_since(std::time::UNIX_EPOCH)
1508 .expect("system time")
1509 .as_secs();
1510
1511 let addresses: Vec<xor_name::XorName> = (0..4u8)
1512 .map(|i| xor_name::XorName::from_content(&[i]))
1513 .collect();
1514 let tree = MerkleTree::from_xornames(addresses.clone()).expect("tree");
1515
1516 let candidate_nodes = make_candidate_nodes(timestamp);
1517
1518 let reward_candidates = tree
1519 .reward_candidates(timestamp)
1520 .expect("reward candidates");
1521 let midpoint_proof = reward_candidates
1522 .first()
1523 .expect("at least one candidate")
1524 .clone();
1525
1526 let pool = MerklePaymentCandidatePool {
1527 midpoint_proof,
1528 candidate_nodes,
1529 };
1530
1531 let first_address = *addresses.first().expect("first address");
1532 let address_proof = tree
1533 .generate_address_proof(0, first_address)
1534 .expect("proof");
1535
1536 let merkle_proof = MerklePaymentProof::new(first_address, address_proof, pool);
1537 let pool_hash = merkle_proof.winner_pool_hash();
1538 let xorname = first_address.0;
1539
1540 (merkle_proof, pool_hash, xorname, timestamp)
1541 }
1542
1543 fn make_valid_merkle_proof_bytes() -> (
1546 [u8; 32],
1547 Vec<u8>,
1548 evmlib::merkle_batch_payment::PoolHash,
1549 u64,
1550 ) {
1551 let (merkle_proof, pool_hash, xorname, timestamp) = make_valid_merkle_proof();
1552 let tagged = crate::payment::proof::serialize_merkle_proof(&merkle_proof)
1553 .expect("serialize merkle proof");
1554 (xorname, tagged, pool_hash, timestamp)
1555 }
1556
1557 #[tokio::test]
1558 async fn test_merkle_address_mismatch_rejected() {
1559 let verifier = create_test_verifier();
1560 let (_correct_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1561
1562 let wrong_xorname = [0xFFu8; 32];
1564
1565 let result = verifier
1566 .verify_payment(&wrong_xorname, Some(&tagged_proof))
1567 .await;
1568
1569 assert!(
1570 result.is_err(),
1571 "Should reject merkle proof address mismatch"
1572 );
1573 let err_msg = format!("{}", result.expect_err("should fail"));
1574 assert!(
1575 err_msg.contains("address mismatch") || err_msg.contains("Merkle proof address"),
1576 "Error should mention address mismatch: {err_msg}"
1577 );
1578 }
1579
1580 #[tokio::test]
1581 async fn test_merkle_malformed_body_rejected() {
1582 let verifier = create_test_verifier();
1583 let xorname = [0xA3u8; 32];
1584
1585 let mut bad_proof = vec![crate::ant_protocol::PROOF_TAG_MERKLE];
1587 bad_proof.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
1588 bad_proof.extend_from_slice(&[0x00; 10]);
1589 while bad_proof.len() < MIN_PAYMENT_PROOF_SIZE_BYTES {
1591 bad_proof.push(0x00);
1592 }
1593
1594 let result = verifier.verify_payment(&xorname, Some(&bad_proof)).await;
1595
1596 assert!(result.is_err(), "Should reject malformed merkle body");
1597 let err_msg = format!("{}", result.expect_err("should fail"));
1598 assert!(
1599 err_msg.contains("deserialize") || err_msg.contains("Failed"),
1600 "Error should mention deserialization: {err_msg}"
1601 );
1602 }
1603
1604 #[test]
1605 fn test_merkle_proof_serialized_size_within_limits() {
1606 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1607
1608 assert!(
1610 tagged_proof.len() >= MIN_PAYMENT_PROOF_SIZE_BYTES,
1611 "Merkle proof ({} bytes) should be >= min {} bytes",
1612 tagged_proof.len(),
1613 MIN_PAYMENT_PROOF_SIZE_BYTES
1614 );
1615 assert!(
1616 tagged_proof.len() <= MAX_PAYMENT_PROOF_SIZE_BYTES,
1617 "Merkle proof ({} bytes) should be <= max {} bytes",
1618 tagged_proof.len(),
1619 MAX_PAYMENT_PROOF_SIZE_BYTES
1620 );
1621 }
1622
1623 #[test]
1624 fn test_merkle_proof_tag_is_correct() {
1625 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1626
1627 assert_eq!(
1628 tagged_proof.first().copied(),
1629 Some(crate::ant_protocol::PROOF_TAG_MERKLE),
1630 "First byte must be the merkle tag"
1631 );
1632 assert_eq!(
1633 crate::payment::proof::detect_proof_type(&tagged_proof),
1634 Some(crate::payment::proof::ProofType::Merkle)
1635 );
1636 }
1637
1638 #[test]
1639 fn test_pool_cache_eviction() {
1640 use evmlib::merkle_batch_payment::PoolHash;
1641
1642 let config = PaymentVerifierConfig {
1643 evm: EvmVerifierConfig::default(),
1644 cache_capacity: 100,
1645 local_rewards_address: RewardsAddress::new([1u8; 20]),
1646 };
1647 let verifier = PaymentVerifier::new(config);
1648
1649 for i in 0..DEFAULT_POOL_CACHE_CAPACITY {
1651 let mut hash: PoolHash = [0u8; 32];
1652 let idx_bytes = i.to_le_bytes();
1654 for (j, b) in idx_bytes.iter().enumerate() {
1655 if j < 32 {
1656 hash[j] = *b;
1657 }
1658 }
1659 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1660 depth: 4,
1661 merkle_payment_timestamp: 1_700_000_000,
1662 paid_node_addresses: vec![],
1663 };
1664 verifier.pool_cache.lock().put(hash, info);
1665 }
1666
1667 assert_eq!(
1668 verifier.pool_cache.lock().len(),
1669 DEFAULT_POOL_CACHE_CAPACITY
1670 );
1671
1672 let overflow_hash: PoolHash = [0xFFu8; 32];
1674 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1675 depth: 8,
1676 merkle_payment_timestamp: 1_800_000_000,
1677 paid_node_addresses: vec![],
1678 };
1679 verifier.pool_cache.lock().put(overflow_hash, info);
1680
1681 assert_eq!(
1683 verifier.pool_cache.lock().len(),
1684 DEFAULT_POOL_CACHE_CAPACITY
1685 );
1686
1687 let found = verifier.pool_cache.lock().get(&overflow_hash).cloned();
1689 assert!(
1690 found.is_some(),
1691 "Newly inserted pool hash should be present"
1692 );
1693 assert_eq!(found.expect("info").depth, 8);
1694 }
1695
1696 #[test]
1697 fn test_pool_cache_concurrent_access() {
1698 use evmlib::merkle_batch_payment::PoolHash;
1699 use std::sync::Arc;
1700
1701 let verifier = Arc::new(create_test_verifier());
1702
1703 let mut handles = Vec::new();
1704 for i in 0..20u8 {
1705 let v = verifier.clone();
1706 handles.push(std::thread::spawn(move || {
1707 let hash: PoolHash = [i; 32];
1708 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1709 depth: i,
1710 merkle_payment_timestamp: u64::from(i) * 1000,
1711 paid_node_addresses: vec![],
1712 };
1713 v.pool_cache.lock().put(hash, info);
1714
1715 let found = v.pool_cache.lock().get(&hash).cloned();
1717 assert!(found.is_some(), "Entry {i} should be readable after insert");
1718 }));
1719 }
1720
1721 for handle in handles {
1722 handle.join().expect("thread panicked");
1723 }
1724
1725 assert_eq!(verifier.pool_cache.lock().len(), 20);
1727 }
1728
1729 #[tokio::test]
1730 async fn test_merkle_tampered_candidate_signature_rejected() {
1731 let verifier = create_test_verifier();
1732
1733 let (mut merkle_proof, _pool_hash, xorname, timestamp) = make_valid_merkle_proof();
1734
1735 if let Some(byte) = merkle_proof
1737 .winner_pool
1738 .candidate_nodes
1739 .first_mut()
1740 .and_then(|c| c.signature.first_mut())
1741 {
1742 *byte ^= 0xFF;
1743 }
1744
1745 let tampered_pool_hash = merkle_proof.winner_pool_hash();
1747
1748 {
1750 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1751 depth: 4,
1752 merkle_payment_timestamp: timestamp,
1753 paid_node_addresses: vec![],
1754 };
1755 verifier.pool_cache.lock().put(tampered_pool_hash, info);
1756 }
1757
1758 let tagged =
1759 crate::payment::proof::serialize_merkle_proof(&merkle_proof).expect("serialize");
1760
1761 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
1762
1763 assert!(
1764 result.is_err(),
1765 "Should reject merkle proof with tampered candidate signature"
1766 );
1767 let err_msg = format!("{}", result.expect_err("should fail"));
1768 assert!(
1769 err_msg.contains("Invalid ML-DSA-65 signature"),
1770 "Error should mention invalid signature: {err_msg}"
1771 );
1772 }
1773
1774 #[tokio::test]
1775 async fn test_merkle_timestamp_mismatch_rejected() {
1776 let verifier = create_test_verifier();
1777
1778 let (xorname, tagged, pool_hash, timestamp) = make_valid_merkle_proof_bytes();
1779
1780 {
1782 let mismatched_ts = timestamp + 9999;
1783 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1784 depth: 4,
1785 merkle_payment_timestamp: mismatched_ts,
1786 paid_node_addresses: vec![],
1787 };
1788 verifier.pool_cache.lock().put(pool_hash, info);
1789 }
1790
1791 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
1792
1793 assert!(
1794 result.is_err(),
1795 "Should reject merkle proof with timestamp mismatch"
1796 );
1797 let err_msg = format!("{}", result.expect_err("should fail"));
1798 assert!(
1799 err_msg.contains("timestamp mismatch"),
1800 "Error should mention timestamp mismatch: {err_msg}"
1801 );
1802 }
1803
1804 #[tokio::test]
1805 async fn test_merkle_paid_node_index_out_of_bounds_rejected() {
1806 let verifier = create_test_verifier();
1807 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1808
1809 {
1813 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1814 depth: 2,
1815 merkle_payment_timestamp: ts,
1816 paid_node_addresses: vec![
1817 (RewardsAddress::new([0u8; 20]), 0),
1819 (RewardsAddress::new([1u8; 20]), 999),
1821 ],
1822 };
1823 verifier.pool_cache.lock().put(pool_hash, info);
1824 }
1825
1826 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1827
1828 assert!(
1829 result.is_err(),
1830 "Should reject paid node index out of bounds"
1831 );
1832 let err_msg = format!("{}", result.expect_err("should fail"));
1833 assert!(
1834 err_msg.contains("out of bounds"),
1835 "Error should mention out of bounds: {err_msg}"
1836 );
1837 }
1838
1839 #[tokio::test]
1840 async fn test_merkle_paid_node_address_mismatch_rejected() {
1841 let verifier = create_test_verifier();
1842 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1843
1844 {
1847 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1848 depth: 2,
1849 merkle_payment_timestamp: ts,
1850 paid_node_addresses: vec![
1851 (RewardsAddress::new([0u8; 20]), 0),
1853 (RewardsAddress::new([0xFF; 20]), 1),
1855 ],
1856 };
1857 verifier.pool_cache.lock().put(pool_hash, info);
1858 }
1859
1860 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1861
1862 assert!(result.is_err(), "Should reject paid node address mismatch");
1863 let err_msg = format!("{}", result.expect_err("should fail"));
1864 assert!(
1865 err_msg.contains("address mismatch"),
1866 "Error should mention address mismatch: {err_msg}"
1867 );
1868 }
1869
1870 #[tokio::test]
1871 async fn test_merkle_wrong_depth_rejected() {
1872 let verifier = create_test_verifier();
1873 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1874
1875 {
1878 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1879 depth: 3,
1880 merkle_payment_timestamp: ts,
1881 paid_node_addresses: vec![(RewardsAddress::new([0u8; 20]), 0)],
1882 };
1883 verifier.pool_cache.lock().put(pool_hash, info);
1884 }
1885
1886 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1887
1888 assert!(
1889 result.is_err(),
1890 "Should reject mismatched depth vs paid node count"
1891 );
1892 let err_msg = format!("{}", result.expect_err("should fail"));
1893 assert!(
1894 err_msg.contains("Wrong number of paid nodes")
1895 || err_msg.contains("verification failed"),
1896 "Error should mention depth/count mismatch: {err_msg}"
1897 );
1898 }
1899}