1use crate::ant_protocol::CLOSE_GROUP_SIZE;
7use crate::error::{Error, Result};
8use crate::logging::{debug, info};
9use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
10use crate::payment::proof::{
11 deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
12};
13use crate::payment::quote::{verify_quote_content, verify_quote_signature};
14use crate::payment::single_node::SingleNodePayment;
15use evmlib::common::Amount;
16use evmlib::contract::payment_vault;
17use evmlib::merkle_batch_payment::{OnChainPaymentInfo, PoolHash};
18use evmlib::Network as EvmNetwork;
19use evmlib::ProofOfPayment;
20use evmlib::RewardsAddress;
21use lru::LruCache;
22use parking_lot::Mutex;
23use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
24use std::num::NonZeroUsize;
25use std::time::SystemTime;
26
27pub const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
32
33pub const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 262_144;
40
41const QUOTE_MAX_AGE_SECS: u64 = 86_400;
44
45const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
48
49#[derive(Debug, Clone)]
54pub struct EvmVerifierConfig {
55 pub network: EvmNetwork,
57}
58
59impl Default for EvmVerifierConfig {
60 fn default() -> Self {
61 Self {
62 network: EvmNetwork::ArbitrumOne,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
72pub struct PaymentVerifierConfig {
73 pub evm: EvmVerifierConfig,
75 pub cache_capacity: usize,
77 pub local_rewards_address: RewardsAddress,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PaymentStatus {
85 CachedAsVerified,
87 PaymentRequired,
89 PaymentVerified,
91}
92
93impl PaymentStatus {
94 #[must_use]
96 pub fn can_store(&self) -> bool {
97 matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
98 }
99
100 #[must_use]
102 pub fn is_cached(&self) -> bool {
103 matches!(self, Self::CachedAsVerified)
104 }
105}
106
107const DEFAULT_POOL_CACHE_CAPACITY: usize = 1_000;
109
110pub struct PaymentVerifier {
117 cache: VerifiedCache,
119 pool_cache: Mutex<LruCache<PoolHash, OnChainPaymentInfo>>,
121 config: PaymentVerifierConfig,
123}
124
125impl PaymentVerifier {
126 #[must_use]
128 pub fn new(config: PaymentVerifierConfig) -> Self {
129 const _: () = assert!(
130 DEFAULT_POOL_CACHE_CAPACITY > 0,
131 "pool cache capacity must be > 0"
132 );
133 let cache = VerifiedCache::with_capacity(config.cache_capacity);
134 let pool_cache_size =
135 NonZeroUsize::new(DEFAULT_POOL_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN);
136 let pool_cache = Mutex::new(LruCache::new(pool_cache_size));
137
138 let cache_capacity = config.cache_capacity;
139 info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on, pool_cache={DEFAULT_POOL_CACHE_CAPACITY})");
140
141 Self {
142 cache,
143 pool_cache,
144 config,
145 }
146 }
147
148 pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
163 if self.cache.contains(xorname) {
165 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
166 debug!("Data {} found in verified cache", hex::encode(xorname));
167 }
168 return PaymentStatus::CachedAsVerified;
169 }
170
171 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
173 debug!(
174 "Data {} not in cache - payment required",
175 hex::encode(xorname)
176 );
177 }
178 PaymentStatus::PaymentRequired
179 }
180
181 pub async fn verify_payment(
201 &self,
202 xorname: &XorName,
203 payment_proof: Option<&[u8]>,
204 ) -> Result<PaymentStatus> {
205 let status = self.check_payment_required(xorname);
207
208 match status {
209 PaymentStatus::CachedAsVerified => {
210 Ok(status)
212 }
213 PaymentStatus::PaymentRequired => {
214 if let Some(proof) = payment_proof {
216 let proof_len = proof.len();
217 if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
218 return Err(Error::Payment(format!(
219 "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
220 )));
221 }
222 if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
223 return Err(Error::Payment(format!(
224 "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
225 )));
226 }
227
228 match detect_proof_type(proof) {
230 Some(ProofType::Merkle) => {
231 self.verify_merkle_payment(xorname, proof).await?;
232 }
233 Some(ProofType::SingleNode) => {
234 let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
235 Error::Payment(format!("Failed to deserialize payment proof: {e}"))
236 })?;
237
238 if !tx_hashes.is_empty() {
239 debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
240 }
241
242 self.verify_evm_payment(xorname, &payment).await?;
243 }
244 None => {
245 let tag = proof.first().copied().unwrap_or(0);
246 return Err(Error::Payment(format!(
247 "Unknown payment proof type tag: 0x{tag:02x}"
248 )));
249 }
250 }
251
252 self.cache.insert(*xorname);
254
255 Ok(PaymentStatus::PaymentVerified)
256 } else {
257 let xorname_hex = hex::encode(xorname);
259 Err(Error::Payment(format!(
260 "Payment required for new data {xorname_hex}"
261 )))
262 }
263 }
264 PaymentStatus::PaymentVerified => Err(Error::Payment(
265 "Unexpected PaymentVerified status from check_payment_required".to_string(),
266 )),
267 }
268 }
269
270 #[must_use]
272 pub fn cache_stats(&self) -> CacheStats {
273 self.cache.stats()
274 }
275
276 #[must_use]
278 pub fn cache_len(&self) -> usize {
279 self.cache.len()
280 }
281
282 #[cfg(any(test, feature = "test-utils"))]
288 pub fn cache_insert(&self, xorname: XorName) {
289 self.cache.insert(xorname);
290 }
291
292 async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
308 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
309 let xorname_hex = hex::encode(xorname);
310 let quote_count = payment.peer_quotes.len();
311 debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
312 }
313
314 Self::validate_quote_structure(payment)?;
315 Self::validate_quote_content(payment, xorname)?;
316 Self::validate_quote_timestamps(payment)?;
317 Self::validate_peer_bindings(payment)?;
318 self.validate_local_recipient(payment)?;
319
320 let peer_quotes = payment.peer_quotes.clone();
322 tokio::task::spawn_blocking(move || {
323 for (encoded_peer_id, quote) in &peer_quotes {
324 if !verify_quote_signature(quote) {
325 return Err(Error::Payment(
326 format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
327 ));
328 }
329 }
330 Ok(())
331 })
332 .await
333 .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
334
335 let quotes_with_prices: Vec<_> = payment
338 .peer_quotes
339 .iter()
340 .map(|(_, quote)| (quote.clone(), quote.price))
341 .collect();
342 let single_payment = SingleNodePayment::from_quotes(quotes_with_prices).map_err(|e| {
343 Error::Payment(format!(
344 "Failed to reconstruct payment for verification: {e}"
345 ))
346 })?;
347
348 let verified_amount = single_payment
351 .verify(&self.config.evm.network)
352 .await
353 .map_err(|e| {
354 let xorname_hex = hex::encode(xorname);
355 Error::Payment(format!(
356 "Median quote payment verification failed for {xorname_hex}: {e}"
357 ))
358 })?;
359
360 if crate::logging::enabled!(crate::logging::Level::INFO) {
361 let xorname_hex = hex::encode(xorname);
362 info!("EVM payment verified for {xorname_hex} (median paid {verified_amount} atto)");
363 }
364 Ok(())
365 }
366
367 fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
369 if payment.peer_quotes.is_empty() {
370 return Err(Error::Payment("Payment has no quotes".to_string()));
371 }
372
373 let quote_count = payment.peer_quotes.len();
374 if quote_count != CLOSE_GROUP_SIZE {
375 return Err(Error::Payment(format!(
376 "Payment must have exactly {CLOSE_GROUP_SIZE} quotes, got {quote_count}"
377 )));
378 }
379
380 let mut seen: Vec<&evmlib::EncodedPeerId> = Vec::with_capacity(quote_count);
381 for (encoded_peer_id, _) in &payment.peer_quotes {
382 if seen.contains(&encoded_peer_id) {
383 return Err(Error::Payment(format!(
384 "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
385 )));
386 }
387 seen.push(encoded_peer_id);
388 }
389
390 Ok(())
391 }
392
393 fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
395 for (encoded_peer_id, quote) in &payment.peer_quotes {
396 if !verify_quote_content(quote, xorname) {
397 let expected_hex = hex::encode(xorname);
398 let actual_hex = hex::encode(quote.content.0);
399 return Err(Error::Payment(format!(
400 "Quote content address mismatch for peer {encoded_peer_id:?}: expected {expected_hex}, got {actual_hex}"
401 )));
402 }
403 }
404 Ok(())
405 }
406
407 fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
409 let now = SystemTime::now();
410 for (encoded_peer_id, quote) in &payment.peer_quotes {
411 match now.duration_since(quote.timestamp) {
412 Ok(age) => {
413 if age.as_secs() > QUOTE_MAX_AGE_SECS {
414 return Err(Error::Payment(format!(
415 "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
416 age.as_secs()
417 )));
418 }
419 }
420 Err(_) => {
421 if let Ok(skew) = quote.timestamp.duration_since(now) {
422 if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
423 return Err(Error::Payment(format!(
424 "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
425 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
426 skew.as_secs()
427 )));
428 }
429 } else {
430 return Err(Error::Payment(format!(
431 "Quote from peer {encoded_peer_id:?} has invalid timestamp"
432 )));
433 }
434 }
435 }
436 }
437 Ok(())
438 }
439
440 fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
442 for (encoded_peer_id, quote) in &payment.peer_quotes {
443 let expected_peer_id = peer_id_from_public_key_bytes("e.pub_key)
444 .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
445
446 if expected_peer_id.as_bytes() != encoded_peer_id.as_bytes() {
447 let expected_hex = expected_peer_id.to_hex();
448 let actual_hex = hex::encode(encoded_peer_id.as_bytes());
449 return Err(Error::Payment(format!(
450 "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
451 BLAKE3(pub_key) = {expected_hex}, peer_id = {actual_hex}"
452 )));
453 }
454 }
455 Ok(())
456 }
457
458 #[allow(clippy::too_many_lines)]
467 async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> {
468 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
469 debug!("Verifying merkle payment for {}", hex::encode(xorname));
470 }
471
472 let merkle_proof = deserialize_merkle_proof(proof_bytes)
474 .map_err(|e| Error::Payment(format!("Failed to deserialize merkle proof: {e}")))?;
475
476 if merkle_proof.address.0 != *xorname {
478 let proof_hex = hex::encode(merkle_proof.address.0);
479 let store_hex = hex::encode(xorname);
480 return Err(Error::Payment(format!(
481 "Merkle proof address mismatch: proof is for {proof_hex}, but storing {store_hex}"
482 )));
483 }
484
485 let pool_hash = merkle_proof.winner_pool_hash();
486
487 for candidate in &merkle_proof.winner_pool.candidate_nodes {
490 if !crate::payment::verify_merkle_candidate_signature(candidate) {
491 return Err(Error::Payment(format!(
492 "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})",
493 candidate.reward_address
494 )));
495 }
496 }
497
498 let cached_info = {
500 let mut pool_cache = self.pool_cache.lock();
501 pool_cache.get(&pool_hash).cloned()
502 };
503
504 let payment_info = if let Some(info) = cached_info {
505 debug!("Pool cache hit for hash {}", hex::encode(pool_hash));
506 info
507 } else {
508 let info =
510 payment_vault::get_completed_merkle_payment(&self.config.evm.network, pool_hash)
511 .await
512 .map_err(|e| {
513 let pool_hex = hex::encode(pool_hash);
514 Error::Payment(format!(
515 "Failed to query merkle payment info for pool {pool_hex}: {e}"
516 ))
517 })?;
518
519 let paid_node_addresses: Vec<_> = info
520 .paidNodeAddresses
521 .iter()
522 .map(|pna| (pna.rewardsAddress, usize::from(pna.poolIndex), pna.amount))
523 .collect();
524
525 let on_chain_info = OnChainPaymentInfo {
526 depth: info.depth,
527 merkle_payment_timestamp: info.merklePaymentTimestamp,
528 paid_node_addresses,
529 };
530
531 {
533 let mut pool_cache = self.pool_cache.lock();
534 pool_cache.put(pool_hash, on_chain_info.clone());
535 }
536
537 debug!(
538 "Queried on-chain merkle payment info for pool {}: depth={}, timestamp={}, paid_nodes={}",
539 hex::encode(pool_hash),
540 on_chain_info.depth,
541 on_chain_info.merkle_payment_timestamp,
542 on_chain_info.paid_node_addresses.len()
543 );
544
545 on_chain_info
546 };
547
548 for candidate in &merkle_proof.winner_pool.candidate_nodes {
550 if candidate.merkle_payment_timestamp != payment_info.merkle_payment_timestamp {
551 return Err(Error::Payment(format!(
552 "Candidate timestamp mismatch: expected {}, got {} (reward: {})",
553 payment_info.merkle_payment_timestamp,
554 candidate.merkle_payment_timestamp,
555 candidate.reward_address
556 )));
557 }
558 }
559
560 let smart_contract_root = merkle_proof.winner_pool.midpoint_proof.root();
562
563 evmlib::merkle_payments::verify_merkle_proof(
566 &merkle_proof.address,
567 &merkle_proof.data_proof,
568 &merkle_proof.winner_pool.midpoint_proof,
569 payment_info.depth,
570 smart_contract_root,
571 payment_info.merkle_payment_timestamp,
572 )
573 .map_err(|e| {
574 let xorname_hex = hex::encode(xorname);
575 Error::Payment(format!(
576 "Merkle proof verification failed for {xorname_hex}: {e}"
577 ))
578 })?;
579
580 let expected_depth = payment_info.depth as usize;
582 let actual_paid = payment_info.paid_node_addresses.len();
583 if actual_paid != expected_depth {
584 return Err(Error::Payment(format!(
585 "Wrong number of paid nodes: expected {expected_depth}, got {actual_paid}"
586 )));
587 }
588
589 let expected_per_node = if payment_info.depth > 0 {
593 let mut candidate_prices: Vec<Amount> = merkle_proof
594 .winner_pool
595 .candidate_nodes
596 .iter()
597 .map(|c| c.price)
598 .collect();
599 candidate_prices.sort_unstable(); let median_price = *candidate_prices
602 .get(candidate_prices.len() / 2)
603 .ok_or_else(|| Error::Payment("empty candidate pool in merkle proof".into()))?;
604 let shift = u32::from(payment_info.depth);
605 let multiplier = 1u64
606 .checked_shl(shift)
607 .ok_or_else(|| Error::Payment("merkle proof depth too large".into()))?;
608 let total_amount = median_price * Amount::from(multiplier);
609 total_amount / Amount::from(u64::from(payment_info.depth))
610 } else {
611 Amount::ZERO
612 };
613
614 for (addr, idx, paid_amount) in &payment_info.paid_node_addresses {
630 let node = merkle_proof
631 .winner_pool
632 .candidate_nodes
633 .get(*idx)
634 .ok_or_else(|| {
635 Error::Payment(format!(
636 "Paid node index {idx} out of bounds for pool size {}",
637 merkle_proof.winner_pool.candidate_nodes.len()
638 ))
639 })?;
640 if node.reward_address != *addr {
641 return Err(Error::Payment(format!(
642 "Paid node address mismatch at index {idx}: expected {addr}, got {}",
643 node.reward_address
644 )));
645 }
646 if *paid_amount < expected_per_node {
647 return Err(Error::Payment(format!(
648 "Underpayment for node at index {idx}: paid {paid_amount}, \
649 expected at least {expected_per_node} \
650 (median16 formula, depth={})",
651 payment_info.depth
652 )));
653 }
654 }
655
656 if crate::logging::enabled!(crate::logging::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(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 7-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 7-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::{EncodedPeerId, PaymentQuote, RewardsAddress};
996 use std::time::SystemTime;
997
998 let verifier = create_test_verifier();
999
1000 let target_xorname = [0xAAu8; 32];
1002
1003 let wrong_xorname = [0xBBu8; 32];
1005 let quote = PaymentQuote {
1006 content: xor_name::XorName(wrong_xorname),
1007 timestamp: SystemTime::now(),
1008 price: Amount::from(1u64),
1009 rewards_address: RewardsAddress::new([1u8; 20]),
1010 pub_key: vec![0u8; 64],
1011 signature: vec![0u8; 64],
1012 };
1013
1014 let mut peer_quotes = Vec::new();
1016 for _ in 0..CLOSE_GROUP_SIZE {
1017 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1018 }
1019
1020 let proof = PaymentProof {
1021 proof_of_payment: ProofOfPayment { peer_quotes },
1022 tx_hashes: vec![],
1023 };
1024
1025 let proof_bytes = serialize_single_node_proof(&proof).expect("serialize proof");
1026
1027 let result = verifier
1028 .verify_payment(&target_xorname, Some(&proof_bytes))
1029 .await;
1030
1031 assert!(result.is_err(), "Should reject mismatched content address");
1032 let err_msg = format!("{}", result.expect_err("should be error"));
1033 assert!(
1034 err_msg.contains("content address mismatch"),
1035 "Error should mention 'content address mismatch': {err_msg}"
1036 );
1037 }
1038
1039 fn make_fake_quote(
1041 xorname: [u8; 32],
1042 timestamp: SystemTime,
1043 rewards_address: RewardsAddress,
1044 ) -> evmlib::PaymentQuote {
1045 use evmlib::PaymentQuote;
1046
1047 PaymentQuote {
1048 content: xor_name::XorName(xorname),
1049 timestamp,
1050 price: Amount::from(1u64),
1051 rewards_address,
1052 pub_key: vec![0u8; 64],
1053 signature: vec![0u8; 64],
1054 }
1055 }
1056
1057 fn serialize_proof(peer_quotes: Vec<(evmlib::EncodedPeerId, evmlib::PaymentQuote)>) -> Vec<u8> {
1059 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1060
1061 let proof = PaymentProof {
1062 proof_of_payment: ProofOfPayment { peer_quotes },
1063 tx_hashes: vec![],
1064 };
1065 serialize_single_node_proof(&proof).expect("serialize proof")
1066 }
1067
1068 #[tokio::test]
1069 async fn test_expired_quote_rejected() {
1070 use evmlib::{EncodedPeerId, RewardsAddress};
1071 use std::time::Duration;
1072
1073 let verifier = create_test_verifier();
1074 let xorname = [0xCCu8; 32];
1075 let rewards_addr = RewardsAddress::new([1u8; 20]);
1076
1077 let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
1079 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1080
1081 let mut peer_quotes = Vec::new();
1082 for _ in 0..CLOSE_GROUP_SIZE {
1083 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1084 }
1085
1086 let proof_bytes = serialize_proof(peer_quotes);
1087 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1088
1089 assert!(result.is_err(), "Should reject expired quote");
1090 let err_msg = format!("{}", result.expect_err("should fail"));
1091 assert!(
1092 err_msg.contains("expired"),
1093 "Error should mention 'expired': {err_msg}"
1094 );
1095 }
1096
1097 #[tokio::test]
1098 async fn test_future_timestamp_rejected() {
1099 use evmlib::{EncodedPeerId, RewardsAddress};
1100 use std::time::Duration;
1101
1102 let verifier = create_test_verifier();
1103 let xorname = [0xDDu8; 32];
1104 let rewards_addr = RewardsAddress::new([1u8; 20]);
1105
1106 let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
1108 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1109
1110 let mut peer_quotes = Vec::new();
1111 for _ in 0..CLOSE_GROUP_SIZE {
1112 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1113 }
1114
1115 let proof_bytes = serialize_proof(peer_quotes);
1116 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1117
1118 assert!(result.is_err(), "Should reject future-timestamped quote");
1119 let err_msg = format!("{}", result.expect_err("should fail"));
1120 assert!(
1121 err_msg.contains("future"),
1122 "Error should mention 'future': {err_msg}"
1123 );
1124 }
1125
1126 #[tokio::test]
1127 async fn test_quote_within_clock_skew_tolerance_accepted() {
1128 use evmlib::{EncodedPeerId, RewardsAddress};
1129 use std::time::Duration;
1130
1131 let verifier = create_test_verifier();
1132 let xorname = [0xD1u8; 32];
1133 let rewards_addr = RewardsAddress::new([1u8; 20]);
1134
1135 let future_timestamp = SystemTime::now() + Duration::from_secs(30);
1137 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1138
1139 let mut peer_quotes = Vec::new();
1140 for _ in 0..CLOSE_GROUP_SIZE {
1141 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1142 }
1143
1144 let proof_bytes = serialize_proof(peer_quotes);
1145 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1146
1147 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1149 assert!(
1150 !err_msg.contains("future"),
1151 "Should pass timestamp check (within tolerance), but got: {err_msg}"
1152 );
1153 }
1154
1155 #[tokio::test]
1156 async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
1157 use evmlib::{EncodedPeerId, RewardsAddress};
1158 use std::time::Duration;
1159
1160 let verifier = create_test_verifier();
1161 let xorname = [0xD2u8; 32];
1162 let rewards_addr = RewardsAddress::new([1u8; 20]);
1163
1164 let future_timestamp = SystemTime::now() + Duration::from_secs(120);
1166 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1167
1168 let mut peer_quotes = Vec::new();
1169 for _ in 0..CLOSE_GROUP_SIZE {
1170 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1171 }
1172
1173 let proof_bytes = serialize_proof(peer_quotes);
1174 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1175
1176 assert!(
1177 result.is_err(),
1178 "Should reject quote beyond clock skew tolerance"
1179 );
1180 let err_msg = format!("{}", result.expect_err("should fail"));
1181 assert!(
1182 err_msg.contains("future"),
1183 "Error should mention 'future': {err_msg}"
1184 );
1185 }
1186
1187 #[tokio::test]
1188 async fn test_quote_23h_old_still_accepted() {
1189 use evmlib::{EncodedPeerId, RewardsAddress};
1190 use std::time::Duration;
1191
1192 let verifier = create_test_verifier();
1193 let xorname = [0xD3u8; 32];
1194 let rewards_addr = RewardsAddress::new([1u8; 20]);
1195
1196 let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
1198 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1199
1200 let mut peer_quotes = Vec::new();
1201 for _ in 0..CLOSE_GROUP_SIZE {
1202 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1203 }
1204
1205 let proof_bytes = serialize_proof(peer_quotes);
1206 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1207
1208 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1210 assert!(
1211 !err_msg.contains("expired"),
1212 "Should pass expiry check (23h < 24h), but got: {err_msg}"
1213 );
1214 }
1215
1216 fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> evmlib::EncodedPeerId {
1218 let ant_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1219 evmlib::EncodedPeerId::new(*ant_peer_id.as_bytes())
1220 }
1221
1222 #[tokio::test]
1223 async fn test_local_not_in_paid_set_rejected() {
1224 use evmlib::RewardsAddress;
1225 use saorsa_core::MlDsa65;
1226 use saorsa_pqc::pqc::MlDsaOperations;
1227
1228 let local_addr = RewardsAddress::new([0xAAu8; 20]);
1230 let config = PaymentVerifierConfig {
1231 evm: EvmVerifierConfig {
1232 network: EvmNetwork::ArbitrumOne,
1233 },
1234 cache_capacity: 100,
1235 local_rewards_address: local_addr,
1236 };
1237 let verifier = PaymentVerifier::new(config);
1238
1239 let xorname = [0xEEu8; 32];
1240 let other_addr = RewardsAddress::new([0xBBu8; 20]);
1242
1243 let ml_dsa = MlDsa65::new();
1245 let mut peer_quotes = Vec::new();
1246 for _ in 0..CLOSE_GROUP_SIZE {
1247 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1248 let pub_key_bytes = public_key.as_bytes().to_vec();
1249 let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1250
1251 let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1252 quote.pub_key = pub_key_bytes;
1253
1254 peer_quotes.push((encoded, quote));
1255 }
1256
1257 let proof_bytes = serialize_proof(peer_quotes);
1258 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1259
1260 assert!(result.is_err(), "Should reject payment not addressed to us");
1261 let err_msg = format!("{}", result.expect_err("should fail"));
1262 assert!(
1263 err_msg.contains("does not include this node as a recipient"),
1264 "Error should mention recipient rejection: {err_msg}"
1265 );
1266 }
1267
1268 #[tokio::test]
1269 async fn test_wrong_peer_binding_rejected() {
1270 use evmlib::{EncodedPeerId, RewardsAddress};
1271 use saorsa_core::MlDsa65;
1272 use saorsa_pqc::pqc::MlDsaOperations;
1273
1274 let verifier = create_test_verifier();
1275 let xorname = [0xFFu8; 32];
1276 let rewards_addr = RewardsAddress::new([1u8; 20]);
1277
1278 let ml_dsa = MlDsa65::new();
1280 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1281 let pub_key_bytes = public_key.as_bytes().to_vec();
1282
1283 let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1286 quote.pub_key = pub_key_bytes;
1287
1288 let mut peer_quotes = Vec::new();
1290 for _ in 0..CLOSE_GROUP_SIZE {
1291 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1292 }
1293
1294 let proof_bytes = serialize_proof(peer_quotes);
1295 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1296
1297 assert!(result.is_err(), "Should reject wrong peer binding");
1298 let err_msg = format!("{}", result.expect_err("should fail"));
1299 assert!(
1300 err_msg.contains("pub_key does not belong to claimed peer"),
1301 "Error should mention binding mismatch: {err_msg}"
1302 );
1303 }
1304
1305 #[tokio::test]
1310 async fn test_merkle_tagged_proof_invalid_data_rejected() {
1311 use crate::ant_protocol::PROOF_TAG_MERKLE;
1312
1313 let verifier = create_test_verifier();
1314 let xorname = [0xA1u8; 32];
1315
1316 let mut merkle_garbage = Vec::with_capacity(64);
1319 merkle_garbage.push(PROOF_TAG_MERKLE);
1320 merkle_garbage.extend_from_slice(&[0xAB; 63]);
1321
1322 let result = verifier
1323 .verify_payment(&xorname, Some(&merkle_garbage))
1324 .await;
1325
1326 assert!(
1327 result.is_err(),
1328 "Should reject merkle proof with invalid body"
1329 );
1330 let err_msg = format!("{}", result.expect_err("should fail"));
1331 assert!(
1332 err_msg.contains("deserialize") || err_msg.contains("merkle proof"),
1333 "Error should mention deserialization failure: {err_msg}"
1334 );
1335 }
1336
1337 #[tokio::test]
1338 async fn test_single_node_tagged_proof_deserialization() {
1339 use crate::payment::proof::serialize_single_node_proof;
1340 use evmlib::{EncodedPeerId, RewardsAddress};
1341
1342 let verifier = create_test_verifier();
1343 let xorname = [0xA2u8; 32];
1344 let rewards_addr = RewardsAddress::new([1u8; 20]);
1345
1346 let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1348 let mut peer_quotes = Vec::new();
1349 for _ in 0..CLOSE_GROUP_SIZE {
1350 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1351 }
1352
1353 let proof = crate::payment::proof::PaymentProof {
1354 proof_of_payment: ProofOfPayment {
1355 peer_quotes: peer_quotes.clone(),
1356 },
1357 tx_hashes: vec![],
1358 };
1359
1360 let tagged_bytes = serialize_single_node_proof(&proof).expect("serialize tagged proof");
1361
1362 assert_eq!(
1364 crate::payment::proof::detect_proof_type(&tagged_bytes),
1365 Some(crate::payment::proof::ProofType::SingleNode)
1366 );
1367
1368 let result = verifier.verify_payment(&xorname, Some(&tagged_bytes)).await;
1372
1373 assert!(result.is_err(), "Should fail at quote validation stage");
1374 let err_msg = format!("{}", result.expect_err("should fail"));
1375 assert!(
1377 !err_msg.contains("deserialize"),
1378 "Should pass deserialization but fail later: {err_msg}"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_pool_cache_insert_and_lookup() {
1384 use evmlib::merkle_batch_payment::PoolHash;
1385
1386 let verifier = create_test_verifier();
1389
1390 let pool_hash: PoolHash = [0xBBu8; 32];
1391 let payment_info = evmlib::merkle_payments::OnChainPaymentInfo {
1392 depth: 4,
1393 merkle_payment_timestamp: 1_700_000_000,
1394 paid_node_addresses: vec![],
1395 };
1396
1397 {
1399 let mut cache = verifier.pool_cache.lock();
1400 cache.put(pool_hash, payment_info);
1401 }
1402
1403 {
1405 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1406 assert!(found.is_some(), "Pool hash should be in cache after insert");
1407 let info = found.expect("cached info");
1408 assert_eq!(info.depth, 4);
1409 assert_eq!(info.merkle_payment_timestamp, 1_700_000_000);
1410 }
1411
1412 {
1414 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1415 assert!(
1416 found.is_some(),
1417 "Pool hash should still be in cache on second lookup"
1418 );
1419 }
1420
1421 let other_hash: PoolHash = [0xCCu8; 32];
1423 {
1424 let found = verifier.pool_cache.lock().get(&other_hash).cloned();
1425 assert!(found.is_none(), "Unknown pool hash should not be in cache");
1426 }
1427 }
1428
1429 fn make_candidate_nodes(
1435 timestamp: u64,
1436 ) -> [evmlib::merkle_payments::MerklePaymentCandidateNode;
1437 evmlib::merkle_payments::CANDIDATES_PER_POOL] {
1438 use evmlib::merkle_payments::{MerklePaymentCandidateNode, CANDIDATES_PER_POOL};
1439 use saorsa_core::MlDsa65;
1440 use saorsa_pqc::pqc::types::MlDsaSecretKey;
1441 use saorsa_pqc::pqc::MlDsaOperations;
1442
1443 std::array::from_fn::<_, CANDIDATES_PER_POOL, _>(|i| {
1444 let ml_dsa = MlDsa65::new();
1445 let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
1446 let price = evmlib::common::Amount::from(1024u64);
1447 #[allow(clippy::cast_possible_truncation)]
1448 let reward_address = RewardsAddress::new([i as u8; 20]);
1449 let msg = MerklePaymentCandidateNode::bytes_to_sign(&price, &reward_address, timestamp);
1450 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
1451 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
1452
1453 MerklePaymentCandidateNode {
1454 pub_key: pub_key.as_bytes().to_vec(),
1455 price,
1456 reward_address,
1457 merkle_payment_timestamp: timestamp,
1458 signature,
1459 }
1460 })
1461 }
1462
1463 fn make_valid_merkle_proof() -> (
1466 evmlib::merkle_payments::MerklePaymentProof,
1467 evmlib::merkle_batch_payment::PoolHash,
1468 [u8; 32],
1469 u64,
1470 ) {
1471 use evmlib::merkle_payments::{MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree};
1472
1473 let timestamp = std::time::SystemTime::now()
1474 .duration_since(std::time::UNIX_EPOCH)
1475 .expect("system time")
1476 .as_secs();
1477
1478 let addresses: Vec<xor_name::XorName> = (0..4u8)
1479 .map(|i| xor_name::XorName::from_content(&[i]))
1480 .collect();
1481 let tree = MerkleTree::from_xornames(addresses.clone()).expect("tree");
1482
1483 let candidate_nodes = make_candidate_nodes(timestamp);
1484
1485 let reward_candidates = tree
1486 .reward_candidates(timestamp)
1487 .expect("reward candidates");
1488 let midpoint_proof = reward_candidates
1489 .first()
1490 .expect("at least one candidate")
1491 .clone();
1492
1493 let pool = MerklePaymentCandidatePool {
1494 midpoint_proof,
1495 candidate_nodes,
1496 };
1497
1498 let first_address = *addresses.first().expect("first address");
1499 let address_proof = tree
1500 .generate_address_proof(0, first_address)
1501 .expect("proof");
1502
1503 let merkle_proof = MerklePaymentProof::new(first_address, address_proof, pool);
1504 let pool_hash = merkle_proof.winner_pool_hash();
1505 let xorname = first_address.0;
1506
1507 (merkle_proof, pool_hash, xorname, timestamp)
1508 }
1509
1510 fn make_valid_merkle_proof_bytes() -> (
1513 [u8; 32],
1514 Vec<u8>,
1515 evmlib::merkle_batch_payment::PoolHash,
1516 u64,
1517 ) {
1518 let (merkle_proof, pool_hash, xorname, timestamp) = make_valid_merkle_proof();
1519 let tagged = crate::payment::proof::serialize_merkle_proof(&merkle_proof)
1520 .expect("serialize merkle proof");
1521 (xorname, tagged, pool_hash, timestamp)
1522 }
1523
1524 #[tokio::test]
1525 async fn test_merkle_address_mismatch_rejected() {
1526 let verifier = create_test_verifier();
1527 let (_correct_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1528
1529 let wrong_xorname = [0xFFu8; 32];
1531
1532 let result = verifier
1533 .verify_payment(&wrong_xorname, Some(&tagged_proof))
1534 .await;
1535
1536 assert!(
1537 result.is_err(),
1538 "Should reject merkle proof address mismatch"
1539 );
1540 let err_msg = format!("{}", result.expect_err("should fail"));
1541 assert!(
1542 err_msg.contains("address mismatch") || err_msg.contains("Merkle proof address"),
1543 "Error should mention address mismatch: {err_msg}"
1544 );
1545 }
1546
1547 #[tokio::test]
1548 async fn test_merkle_malformed_body_rejected() {
1549 let verifier = create_test_verifier();
1550 let xorname = [0xA3u8; 32];
1551
1552 let mut bad_proof = vec![crate::ant_protocol::PROOF_TAG_MERKLE];
1554 bad_proof.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
1555 bad_proof.extend_from_slice(&[0x00; 10]);
1556 while bad_proof.len() < MIN_PAYMENT_PROOF_SIZE_BYTES {
1558 bad_proof.push(0x00);
1559 }
1560
1561 let result = verifier.verify_payment(&xorname, Some(&bad_proof)).await;
1562
1563 assert!(result.is_err(), "Should reject malformed merkle body");
1564 let err_msg = format!("{}", result.expect_err("should fail"));
1565 assert!(
1566 err_msg.contains("deserialize") || err_msg.contains("Failed"),
1567 "Error should mention deserialization: {err_msg}"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_merkle_proof_serialized_size_within_limits() {
1573 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1574
1575 assert!(
1577 tagged_proof.len() >= MIN_PAYMENT_PROOF_SIZE_BYTES,
1578 "Merkle proof ({} bytes) should be >= min {} bytes",
1579 tagged_proof.len(),
1580 MIN_PAYMENT_PROOF_SIZE_BYTES
1581 );
1582 assert!(
1583 tagged_proof.len() <= MAX_PAYMENT_PROOF_SIZE_BYTES,
1584 "Merkle proof ({} bytes) should be <= max {} bytes",
1585 tagged_proof.len(),
1586 MAX_PAYMENT_PROOF_SIZE_BYTES
1587 );
1588 }
1589
1590 #[test]
1591 fn test_merkle_proof_tag_is_correct() {
1592 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
1593
1594 assert_eq!(
1595 tagged_proof.first().copied(),
1596 Some(crate::ant_protocol::PROOF_TAG_MERKLE),
1597 "First byte must be the merkle tag"
1598 );
1599 assert_eq!(
1600 crate::payment::proof::detect_proof_type(&tagged_proof),
1601 Some(crate::payment::proof::ProofType::Merkle)
1602 );
1603 }
1604
1605 #[test]
1606 fn test_pool_cache_eviction() {
1607 use evmlib::merkle_batch_payment::PoolHash;
1608
1609 let config = PaymentVerifierConfig {
1610 evm: EvmVerifierConfig::default(),
1611 cache_capacity: 100,
1612 local_rewards_address: RewardsAddress::new([1u8; 20]),
1613 };
1614 let verifier = PaymentVerifier::new(config);
1615
1616 for i in 0..DEFAULT_POOL_CACHE_CAPACITY {
1618 let mut hash: PoolHash = [0u8; 32];
1619 let idx_bytes = i.to_le_bytes();
1621 for (j, b) in idx_bytes.iter().enumerate() {
1622 if j < 32 {
1623 hash[j] = *b;
1624 }
1625 }
1626 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1627 depth: 4,
1628 merkle_payment_timestamp: 1_700_000_000,
1629 paid_node_addresses: vec![],
1630 };
1631 verifier.pool_cache.lock().put(hash, info);
1632 }
1633
1634 assert_eq!(
1635 verifier.pool_cache.lock().len(),
1636 DEFAULT_POOL_CACHE_CAPACITY
1637 );
1638
1639 let overflow_hash: PoolHash = [0xFFu8; 32];
1641 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1642 depth: 8,
1643 merkle_payment_timestamp: 1_800_000_000,
1644 paid_node_addresses: vec![],
1645 };
1646 verifier.pool_cache.lock().put(overflow_hash, info);
1647
1648 assert_eq!(
1650 verifier.pool_cache.lock().len(),
1651 DEFAULT_POOL_CACHE_CAPACITY
1652 );
1653
1654 let found = verifier.pool_cache.lock().get(&overflow_hash).cloned();
1656 assert!(
1657 found.is_some(),
1658 "Newly inserted pool hash should be present"
1659 );
1660 assert_eq!(found.expect("info").depth, 8);
1661 }
1662
1663 #[test]
1664 fn test_pool_cache_concurrent_access() {
1665 use evmlib::merkle_batch_payment::PoolHash;
1666 use std::sync::Arc;
1667
1668 let verifier = Arc::new(create_test_verifier());
1669
1670 let mut handles = Vec::new();
1671 for i in 0..20u8 {
1672 let v = verifier.clone();
1673 handles.push(std::thread::spawn(move || {
1674 let hash: PoolHash = [i; 32];
1675 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1676 depth: i,
1677 merkle_payment_timestamp: u64::from(i) * 1000,
1678 paid_node_addresses: vec![],
1679 };
1680 v.pool_cache.lock().put(hash, info);
1681
1682 let found = v.pool_cache.lock().get(&hash).cloned();
1684 assert!(found.is_some(), "Entry {i} should be readable after insert");
1685 }));
1686 }
1687
1688 for handle in handles {
1689 handle.join().expect("thread panicked");
1690 }
1691
1692 assert_eq!(verifier.pool_cache.lock().len(), 20);
1694 }
1695
1696 #[tokio::test]
1697 async fn test_merkle_tampered_candidate_signature_rejected() {
1698 let verifier = create_test_verifier();
1699
1700 let (mut merkle_proof, _pool_hash, xorname, timestamp) = make_valid_merkle_proof();
1701
1702 if let Some(byte) = merkle_proof
1704 .winner_pool
1705 .candidate_nodes
1706 .first_mut()
1707 .and_then(|c| c.signature.first_mut())
1708 {
1709 *byte ^= 0xFF;
1710 }
1711
1712 let tampered_pool_hash = merkle_proof.winner_pool_hash();
1714
1715 {
1717 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1718 depth: 4,
1719 merkle_payment_timestamp: timestamp,
1720 paid_node_addresses: vec![],
1721 };
1722 verifier.pool_cache.lock().put(tampered_pool_hash, info);
1723 }
1724
1725 let tagged =
1726 crate::payment::proof::serialize_merkle_proof(&merkle_proof).expect("serialize");
1727
1728 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
1729
1730 assert!(
1731 result.is_err(),
1732 "Should reject merkle proof with tampered candidate signature"
1733 );
1734 let err_msg = format!("{}", result.expect_err("should fail"));
1735 assert!(
1736 err_msg.contains("Invalid ML-DSA-65 signature"),
1737 "Error should mention invalid signature: {err_msg}"
1738 );
1739 }
1740
1741 #[tokio::test]
1742 async fn test_merkle_timestamp_mismatch_rejected() {
1743 let verifier = create_test_verifier();
1744
1745 let (xorname, tagged, pool_hash, timestamp) = make_valid_merkle_proof_bytes();
1746
1747 {
1749 let mismatched_ts = timestamp + 9999;
1750 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1751 depth: 4,
1752 merkle_payment_timestamp: mismatched_ts,
1753 paid_node_addresses: vec![],
1754 };
1755 verifier.pool_cache.lock().put(pool_hash, info);
1756 }
1757
1758 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
1759
1760 assert!(
1761 result.is_err(),
1762 "Should reject merkle proof with timestamp mismatch"
1763 );
1764 let err_msg = format!("{}", result.expect_err("should fail"));
1765 assert!(
1766 err_msg.contains("timestamp mismatch"),
1767 "Error should mention timestamp mismatch: {err_msg}"
1768 );
1769 }
1770
1771 #[tokio::test]
1772 async fn test_merkle_paid_node_index_out_of_bounds_rejected() {
1773 let verifier = create_test_verifier();
1774 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1775
1776 {
1780 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1781 depth: 2,
1782 merkle_payment_timestamp: ts,
1783 paid_node_addresses: vec![
1784 (RewardsAddress::new([0u8; 20]), 0, Amount::from(2048u64)),
1787 (RewardsAddress::new([1u8; 20]), 999, Amount::from(2048u64)),
1789 ],
1790 };
1791 verifier.pool_cache.lock().put(pool_hash, info);
1792 }
1793
1794 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1795
1796 assert!(
1797 result.is_err(),
1798 "Should reject paid node index out of bounds"
1799 );
1800 let err_msg = format!("{}", result.expect_err("should fail"));
1801 assert!(
1802 err_msg.contains("out of bounds"),
1803 "Error should mention out of bounds: {err_msg}"
1804 );
1805 }
1806
1807 #[tokio::test]
1808 async fn test_merkle_paid_node_address_mismatch_rejected() {
1809 let verifier = create_test_verifier();
1810 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1811
1812 {
1815 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1816 depth: 2,
1817 merkle_payment_timestamp: ts,
1818 paid_node_addresses: vec![
1819 (RewardsAddress::new([0u8; 20]), 0, Amount::from(2048u64)),
1822 (RewardsAddress::new([0xFF; 20]), 1, Amount::from(2048u64)),
1824 ],
1825 };
1826 verifier.pool_cache.lock().put(pool_hash, info);
1827 }
1828
1829 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1830
1831 assert!(result.is_err(), "Should reject paid node address mismatch");
1832 let err_msg = format!("{}", result.expect_err("should fail"));
1833 assert!(
1834 err_msg.contains("address mismatch"),
1835 "Error should mention address mismatch: {err_msg}"
1836 );
1837 }
1838
1839 #[tokio::test]
1840 async fn test_merkle_wrong_depth_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: 3,
1849 merkle_payment_timestamp: ts,
1850 paid_node_addresses: vec![(
1851 RewardsAddress::new([0u8; 20]),
1852 0,
1853 Amount::from(1024u64),
1854 )],
1855 };
1856 verifier.pool_cache.lock().put(pool_hash, info);
1857 }
1858
1859 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1860
1861 assert!(
1862 result.is_err(),
1863 "Should reject mismatched depth vs paid node count"
1864 );
1865 let err_msg = format!("{}", result.expect_err("should fail"));
1866 assert!(
1867 err_msg.contains("Wrong number of paid nodes")
1868 || err_msg.contains("verification failed"),
1869 "Error should mention depth/count mismatch: {err_msg}"
1870 );
1871 }
1872
1873 #[tokio::test]
1874 async fn test_merkle_underpayment_rejected() {
1875 let verifier = create_test_verifier();
1876 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
1877
1878 {
1882 let info = evmlib::merkle_payments::OnChainPaymentInfo {
1883 depth: 2,
1884 merkle_payment_timestamp: ts,
1885 paid_node_addresses: vec![
1886 (RewardsAddress::new([0u8; 20]), 0, Amount::from(1u64)),
1887 (RewardsAddress::new([1u8; 20]), 1, Amount::from(1u64)),
1888 ],
1889 };
1890 verifier.pool_cache.lock().put(pool_hash, info);
1891 }
1892
1893 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
1894
1895 assert!(
1896 result.is_err(),
1897 "Should reject merkle payment where paid amount < expected per-node amount"
1898 );
1899 let err_msg = format!("{}", result.expect_err("should fail"));
1900 assert!(
1901 err_msg.contains("Underpayment"),
1902 "Error should mention underpayment: {err_msg}"
1903 );
1904 }
1905}