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::single_node::SingleNodePayment;
14use ant_protocol::payment::verify::{verify_quote_content, verify_quote_signature};
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, RwLock};
23use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
24use saorsa_core::identity::PeerId;
25use saorsa_core::P2PNode;
26use std::num::NonZeroUsize;
27use std::sync::Arc;
28use std::time::SystemTime;
29
30pub const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
35
36pub const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 262_144;
43
44const QUOTE_MAX_AGE_SECS: u64 = 86_400;
47
48const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
51
52#[derive(Debug, Clone)]
57pub struct EvmVerifierConfig {
58 pub network: EvmNetwork,
60}
61
62impl Default for EvmVerifierConfig {
63 fn default() -> Self {
64 Self {
65 network: EvmNetwork::ArbitrumOne,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
75pub struct PaymentVerifierConfig {
76 pub evm: EvmVerifierConfig,
78 pub cache_capacity: usize,
80 pub local_rewards_address: RewardsAddress,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum PaymentStatus {
88 CachedAsVerified,
90 PaymentRequired,
92 PaymentVerified,
94}
95
96impl PaymentStatus {
97 #[must_use]
99 pub fn can_store(&self) -> bool {
100 matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
101 }
102
103 #[must_use]
105 pub fn is_cached(&self) -> bool {
106 matches!(self, Self::CachedAsVerified)
107 }
108}
109
110const DEFAULT_POOL_CACHE_CAPACITY: usize = 1_000;
112
113pub struct PaymentVerifier {
120 cache: VerifiedCache,
122 pool_cache: Mutex<LruCache<PoolHash, OnChainPaymentInfo>>,
124 closeness_pass_cache: Mutex<LruCache<PoolHash, ()>>,
128 inflight_closeness: Mutex<LruCache<PoolHash, Arc<ClosenessSlot>>>,
134 p2p_node: RwLock<Option<Arc<P2PNode>>>,
139 config: PaymentVerifierConfig,
141}
142
143struct ClosenessSlot {
148 notify: Arc<tokio::sync::Notify>,
149 result: std::sync::OnceLock<std::result::Result<(), String>>,
152}
153
154impl ClosenessSlot {
155 fn new() -> Self {
156 Self {
157 notify: Arc::new(tokio::sync::Notify::new()),
158 result: std::sync::OnceLock::new(),
159 }
160 }
161
162 fn notified_owned(&self) -> tokio::sync::futures::OwnedNotified {
168 Arc::clone(&self.notify).notified_owned()
169 }
170}
171
172struct InflightGuard<'a> {
181 slot_cache: &'a Mutex<LruCache<PoolHash, Arc<ClosenessSlot>>>,
182 pool_hash: PoolHash,
183 slot: Arc<ClosenessSlot>,
184}
185
186impl InflightGuard<'_> {
187 fn publish(&self, result: &Result<()>) {
192 let stored: std::result::Result<(), String> = match result {
193 Ok(()) => Ok(()),
194 Err(e) => Err(e.to_string()),
195 };
196 let _ = self.slot.result.set(stored);
197 }
198}
199
200impl Drop for InflightGuard<'_> {
201 fn drop(&mut self) {
202 {
206 let mut cache = self.slot_cache.lock();
207 if let Some(existing) = cache.peek(&self.pool_hash) {
208 if Arc::ptr_eq(existing, &self.slot) {
209 cache.pop(&self.pool_hash);
210 }
211 }
212 }
213 self.slot.notify.notify_waiters();
216 }
217}
218
219impl PaymentVerifier {
220 #[must_use]
222 pub fn new(config: PaymentVerifierConfig) -> Self {
223 const _: () = assert!(
224 DEFAULT_POOL_CACHE_CAPACITY > 0,
225 "pool cache capacity must be > 0"
226 );
227 let cache = VerifiedCache::with_capacity(config.cache_capacity);
228 let pool_cache_size =
229 NonZeroUsize::new(DEFAULT_POOL_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN);
230 let pool_cache = Mutex::new(LruCache::new(pool_cache_size));
231 let closeness_pass_cache = Mutex::new(LruCache::new(pool_cache_size));
232 let inflight_closeness = Mutex::new(LruCache::new(pool_cache_size));
233
234 let cache_capacity = config.cache_capacity;
235 info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on, pool_cache={DEFAULT_POOL_CACHE_CAPACITY})");
236
237 #[cfg(feature = "test-utils")]
242 crate::logging::error!(
243 "PaymentVerifier: built with `test-utils` feature — merkle closeness \
244 defence falls back to fail-open when no P2PNode is attached. This \
245 feature is for test binaries only; production nodes must be built \
246 without it."
247 );
248
249 Self {
250 cache,
251 pool_cache,
252 closeness_pass_cache,
253 inflight_closeness,
254 p2p_node: RwLock::new(None),
255 config,
256 }
257 }
258
259 pub fn attach_p2p_node(&self, node: Arc<P2PNode>) {
268 *self.p2p_node.write() = Some(node);
269 debug!("PaymentVerifier: P2PNode attached for merkle closeness checks");
270 }
271
272 pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
287 if self.cache.contains(xorname) {
289 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
290 debug!("Data {} found in verified cache", hex::encode(xorname));
291 }
292 return PaymentStatus::CachedAsVerified;
293 }
294
295 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
297 debug!(
298 "Data {} not in cache - payment required",
299 hex::encode(xorname)
300 );
301 }
302 PaymentStatus::PaymentRequired
303 }
304
305 pub async fn verify_payment(
325 &self,
326 xorname: &XorName,
327 payment_proof: Option<&[u8]>,
328 ) -> Result<PaymentStatus> {
329 let status = self.check_payment_required(xorname);
331
332 match status {
333 PaymentStatus::CachedAsVerified => {
334 Ok(status)
336 }
337 PaymentStatus::PaymentRequired => {
338 if let Some(proof) = payment_proof {
340 let proof_len = proof.len();
341 if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
342 return Err(Error::Payment(format!(
343 "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
344 )));
345 }
346 if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
347 return Err(Error::Payment(format!(
348 "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
349 )));
350 }
351
352 match detect_proof_type(proof) {
354 Some(ProofType::Merkle) => {
355 self.verify_merkle_payment(xorname, proof).await?;
356 }
357 Some(ProofType::SingleNode) => {
358 let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
359 Error::Payment(format!("Failed to deserialize payment proof: {e}"))
360 })?;
361
362 if !tx_hashes.is_empty() {
363 debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
364 }
365
366 self.verify_evm_payment(xorname, &payment).await?;
367 }
368 None => {
369 let tag = proof.first().copied().unwrap_or(0);
370 return Err(Error::Payment(format!(
371 "Unknown payment proof type tag: 0x{tag:02x}"
372 )));
373 }
374 Some(_) => {
378 let tag = proof.first().copied().unwrap_or(0);
379 return Err(Error::Payment(format!(
380 "Unsupported payment proof type tag: 0x{tag:02x} (this node's protocol version does not handle it — upgrade ant-node)"
381 )));
382 }
383 }
384
385 self.cache.insert(*xorname);
387
388 Ok(PaymentStatus::PaymentVerified)
389 } else {
390 let xorname_hex = hex::encode(xorname);
392 Err(Error::Payment(format!(
393 "Payment required for new data {xorname_hex}"
394 )))
395 }
396 }
397 PaymentStatus::PaymentVerified => Err(Error::Payment(
398 "Unexpected PaymentVerified status from check_payment_required".to_string(),
399 )),
400 }
401 }
402
403 #[must_use]
405 pub fn cache_stats(&self) -> CacheStats {
406 self.cache.stats()
407 }
408
409 #[must_use]
411 pub fn cache_len(&self) -> usize {
412 self.cache.len()
413 }
414
415 #[cfg(any(test, feature = "test-utils"))]
421 pub fn cache_insert(&self, xorname: XorName) {
422 self.cache.insert(xorname);
423 }
424
425 #[cfg(any(test, feature = "test-utils"))]
430 pub fn pool_cache_insert(&self, pool_hash: PoolHash, info: OnChainPaymentInfo) {
431 let mut cache = self.pool_cache.lock();
432 cache.put(pool_hash, info);
433 }
434
435 async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
451 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
452 let xorname_hex = hex::encode(xorname);
453 let quote_count = payment.peer_quotes.len();
454 debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
455 }
456
457 Self::validate_quote_structure(payment)?;
458 Self::validate_quote_content(payment, xorname)?;
459 Self::validate_quote_timestamps(payment)?;
460 Self::validate_peer_bindings(payment)?;
461 self.validate_local_recipient(payment)?;
462
463 let peer_quotes = payment.peer_quotes.clone();
465 tokio::task::spawn_blocking(move || {
466 for (encoded_peer_id, quote) in &peer_quotes {
467 if !verify_quote_signature(quote) {
468 return Err(Error::Payment(
469 format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
470 ));
471 }
472 }
473 Ok(())
474 })
475 .await
476 .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
477
478 let quotes_with_prices: Vec<_> = payment
481 .peer_quotes
482 .iter()
483 .map(|(_, quote)| (quote.clone(), quote.price))
484 .collect();
485 let single_payment = SingleNodePayment::from_quotes(quotes_with_prices).map_err(|e| {
486 Error::Payment(format!(
487 "Failed to reconstruct payment for verification: {e}"
488 ))
489 })?;
490
491 let verified_amount = single_payment
494 .verify(&self.config.evm.network)
495 .await
496 .map_err(|e| {
497 let xorname_hex = hex::encode(xorname);
498 Error::Payment(format!(
499 "Median quote payment verification failed for {xorname_hex}: {e}"
500 ))
501 })?;
502
503 if crate::logging::enabled!(crate::logging::Level::INFO) {
504 let xorname_hex = hex::encode(xorname);
505 info!("EVM payment verified for {xorname_hex} (median paid {verified_amount} atto)");
506 }
507 Ok(())
508 }
509
510 fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
512 if payment.peer_quotes.is_empty() {
513 return Err(Error::Payment("Payment has no quotes".to_string()));
514 }
515
516 let quote_count = payment.peer_quotes.len();
517 if quote_count != CLOSE_GROUP_SIZE {
518 return Err(Error::Payment(format!(
519 "Payment must have exactly {CLOSE_GROUP_SIZE} quotes, got {quote_count}"
520 )));
521 }
522
523 let mut seen: Vec<&evmlib::EncodedPeerId> = Vec::with_capacity(quote_count);
524 for (encoded_peer_id, _) in &payment.peer_quotes {
525 if seen.contains(&encoded_peer_id) {
526 return Err(Error::Payment(format!(
527 "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
528 )));
529 }
530 seen.push(encoded_peer_id);
531 }
532
533 Ok(())
534 }
535
536 fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
538 for (encoded_peer_id, quote) in &payment.peer_quotes {
539 if !verify_quote_content(quote, xorname) {
540 let expected_hex = hex::encode(xorname);
541 let actual_hex = hex::encode(quote.content.0);
542 return Err(Error::Payment(format!(
543 "Quote content address mismatch for peer {encoded_peer_id:?}: expected {expected_hex}, got {actual_hex}"
544 )));
545 }
546 }
547 Ok(())
548 }
549
550 fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
552 let now = SystemTime::now();
553 for (encoded_peer_id, quote) in &payment.peer_quotes {
554 match now.duration_since(quote.timestamp) {
555 Ok(age) => {
556 if age.as_secs() > QUOTE_MAX_AGE_SECS {
557 return Err(Error::Payment(format!(
558 "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
559 age.as_secs()
560 )));
561 }
562 }
563 Err(_) => {
564 if let Ok(skew) = quote.timestamp.duration_since(now) {
565 if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
566 return Err(Error::Payment(format!(
567 "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
568 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
569 skew.as_secs()
570 )));
571 }
572 } else {
573 return Err(Error::Payment(format!(
574 "Quote from peer {encoded_peer_id:?} has invalid timestamp"
575 )));
576 }
577 }
578 }
579 }
580 Ok(())
581 }
582
583 fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
585 for (encoded_peer_id, quote) in &payment.peer_quotes {
586 let expected_peer_id = peer_id_from_public_key_bytes("e.pub_key)
587 .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
588
589 if expected_peer_id.as_bytes() != encoded_peer_id.as_bytes() {
590 let expected_hex = expected_peer_id.to_hex();
591 let actual_hex = hex::encode(encoded_peer_id.as_bytes());
592 return Err(Error::Payment(format!(
593 "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
594 BLAKE3(pub_key) = {expected_hex}, peer_id = {actual_hex}"
595 )));
596 }
597 }
598 Ok(())
599 }
600
601 const CANDIDATE_CLOSENESS_REQUIRED: usize = 13;
619
620 const CLOSENESS_LOOKUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
629
630 const MAX_LEADER_RETRIES: usize = 4;
635
636 async fn verify_merkle_candidate_closeness(
673 &self,
674 pool: &evmlib::merkle_payments::MerklePaymentCandidatePool,
675 pool_hash: PoolHash,
676 ) -> Result<()> {
677 if self.closeness_pass_cache.lock().get(&pool_hash).is_some() {
681 return Ok(());
682 }
683
684 for attempt in 0..=Self::MAX_LEADER_RETRIES {
702 #[allow(clippy::option_if_let_else)]
708 let (waiter_slot, leader_slot) = {
709 let mut inflight = self.inflight_closeness.lock();
710 let chosen = if let Some(existing) = inflight.get(&pool_hash) {
711 (Some(Arc::clone(existing)), None)
712 } else {
713 let slot = Arc::new(ClosenessSlot::new());
714 inflight.put(pool_hash, Arc::clone(&slot));
715 (None, Some(slot))
716 };
717 drop(inflight);
718 chosen
719 };
720
721 if let Some(slot) = waiter_slot {
722 let notified = slot.notified_owned();
728 notified.await;
729
730 if let Some(result) = slot.result.get() {
732 return result.clone().map_err(Error::Payment);
733 }
734 if attempt == Self::MAX_LEADER_RETRIES {
739 return Err(Error::Payment(
740 "Merkle candidate pool rejected: closeness leader \
741 repeatedly failed to publish a result (likely \
742 repeated cancellation or panic)."
743 .into(),
744 ));
745 }
746 continue;
747 }
748
749 let Some(slot) = leader_slot else {
752 return Err(Error::Payment(
754 "internal error: neither leader nor waiter in closeness check".into(),
755 ));
756 };
757 let guard = InflightGuard {
758 slot_cache: &self.inflight_closeness,
759 pool_hash,
760 slot,
761 };
762
763 let result = self.verify_merkle_candidate_closeness_inner(pool).await;
764 guard.publish(&result);
765 if result.is_ok() {
766 self.closeness_pass_cache.lock().put(pool_hash, ());
767 }
768 return result;
769 }
770 Err(Error::Payment(
775 "internal error: closeness retry loop exited without returning".into(),
776 ))
777 }
778
779 fn derive_distinct_candidate_peer_ids(
792 pool: &evmlib::merkle_payments::MerklePaymentCandidatePool,
793 ) -> Result<Vec<PeerId>> {
794 let mut candidate_peer_ids = Vec::with_capacity(pool.candidate_nodes.len());
795 let mut seen = std::collections::HashSet::with_capacity(pool.candidate_nodes.len());
796 for candidate in &pool.candidate_nodes {
797 let pid = peer_id_from_public_key_bytes(&candidate.pub_key).map_err(|e| {
798 Error::Payment(format!(
799 "Invalid ML-DSA public key in merkle candidate: {e}"
800 ))
801 })?;
802 if !seen.insert(pid) {
803 return Err(Error::Payment(
804 "Merkle candidate pool rejected: duplicate candidate PeerId. An \
805 honest pool has 16 distinct candidate pub_keys; duplicates would \
806 let a single real peer satisfy the closeness threshold by being \
807 counted multiple times."
808 .into(),
809 ));
810 }
811 candidate_peer_ids.push(pid);
812 }
813 Ok(candidate_peer_ids)
814 }
815
816 #[allow(clippy::too_many_lines)]
817 async fn verify_merkle_candidate_closeness_inner(
818 &self,
819 pool: &evmlib::merkle_payments::MerklePaymentCandidatePool,
820 ) -> Result<()> {
821 let candidate_peer_ids = Self::derive_distinct_candidate_peer_ids(pool)?;
825
826 let attached = self.p2p_node.read().as_ref().map(Arc::clone);
829 let Some(p2p_node) = attached else {
830 #[cfg(any(test, feature = "test-utils"))]
837 {
838 crate::logging::warn!(
839 "PaymentVerifier: no P2PNode attached; merkle pay-yourself \
840 defence SKIPPED (test build). Production startup MUST call \
841 PaymentVerifier::attach_p2p_node."
842 );
843 return Ok(());
844 }
845 #[cfg(not(any(test, feature = "test-utils")))]
846 {
847 crate::logging::error!(
848 "PaymentVerifier: no P2PNode attached; rejecting merkle \
849 payment. This is a node-startup bug — \
850 PaymentVerifier::attach_p2p_node must be called before \
851 any PUT handler runs."
852 );
853 return Err(Error::Payment(
854 "Merkle candidate pool rejected: verifier is not wired to \
855 the P2P layer; cannot verify candidate closeness."
856 .into(),
857 ));
858 }
859 };
860
861 let pool_address = pool.midpoint_proof.address();
862 let lookup_count = pool.candidate_nodes.len();
863 let network_lookup = p2p_node
864 .dht_manager()
865 .find_closest_nodes_network(&pool_address.0, lookup_count);
866 let network_peers =
867 match tokio::time::timeout(Self::CLOSENESS_LOOKUP_TIMEOUT, network_lookup).await {
868 Ok(Ok(peers)) => peers,
869 Ok(Err(e)) => {
870 debug!(
871 "Merkle closeness network-lookup failed for pool midpoint {}: {e}",
872 hex::encode(pool_address.0),
873 );
874 return Err(Error::Payment(
875 "Merkle candidate pool rejected: could not verify candidate \
876 closeness against the authoritative network view."
877 .into(),
878 ));
879 }
880 Err(_) => {
881 debug!(
882 "Merkle closeness network-lookup timeout ({:?}) for pool midpoint {}",
883 Self::CLOSENESS_LOOKUP_TIMEOUT,
884 hex::encode(pool_address.0),
885 );
886 return Err(Error::Payment(
887 "Merkle candidate pool rejected: authoritative network lookup \
888 timed out. Retry once the network lookup completes."
889 .into(),
890 ));
891 }
892 };
893
894 if network_peers.len() < Self::CANDIDATE_CLOSENESS_REQUIRED {
901 debug!(
902 "Merkle closeness deferred: network lookup returned {} peers \
903 for pool midpoint {} (need at least {} to verify)",
904 network_peers.len(),
905 hex::encode(pool_address.0),
906 Self::CANDIDATE_CLOSENESS_REQUIRED,
907 );
908 return Err(Error::Payment(format!(
909 "Merkle candidate pool rejected: authoritative DHT lookup returned \
910 only {} peers, less than the {} required to verify candidate \
911 closeness. Retry once the routing table populates further.",
912 network_peers.len(),
913 Self::CANDIDATE_CLOSENESS_REQUIRED,
914 )));
915 }
916
917 let network_set: std::collections::HashSet<PeerId> =
921 network_peers.iter().map(|n| n.peer_id).collect();
922 let matched = candidate_peer_ids
923 .iter()
924 .filter(|pid| network_set.contains(pid))
925 .count();
926
927 if matched < Self::CANDIDATE_CLOSENESS_REQUIRED {
928 debug!(
929 "Merkle closeness rejected: {matched}/{} candidates match the DHT's closest peers \
930 for pool midpoint {} (required: {}, network returned {} peers)",
931 pool.candidate_nodes.len(),
932 hex::encode(pool_address.0),
933 Self::CANDIDATE_CLOSENESS_REQUIRED,
934 network_peers.len(),
935 );
936 return Err(Error::Payment(
937 "Merkle candidate pool rejected: candidate pub_keys do not match the \
938 network's closest peers to the pool midpoint address. Pools must be \
939 collected from the pool-address close group, not fabricated off-network."
940 .into(),
941 ));
942 }
943
944 debug!(
945 "Merkle closeness passed: {matched}/{} candidates matched the DHT's closest peers \
946 for pool midpoint {}",
947 pool.candidate_nodes.len(),
948 hex::encode(pool_address.0),
949 );
950 Ok(())
951 }
952
953 #[allow(clippy::too_many_lines)]
962 async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> {
963 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
964 debug!("Verifying merkle payment for {}", hex::encode(xorname));
965 }
966
967 let merkle_proof = deserialize_merkle_proof(proof_bytes)
969 .map_err(|e| Error::Payment(format!("Failed to deserialize merkle proof: {e}")))?;
970
971 if merkle_proof.address.0 != *xorname {
973 let proof_hex = hex::encode(merkle_proof.address.0);
974 let store_hex = hex::encode(xorname);
975 return Err(Error::Payment(format!(
976 "Merkle proof address mismatch: proof is for {proof_hex}, but storing {store_hex}"
977 )));
978 }
979
980 let pool_hash = merkle_proof.winner_pool_hash();
981
982 for candidate in &merkle_proof.winner_pool.candidate_nodes {
985 if !crate::payment::verify_merkle_candidate_signature(candidate) {
986 return Err(Error::Payment(format!(
987 "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})",
988 candidate.reward_address
989 )));
990 }
991 }
992
993 self.verify_merkle_candidate_closeness(&merkle_proof.winner_pool, pool_hash)
1002 .await?;
1003
1004 let cached_info = {
1006 let mut pool_cache = self.pool_cache.lock();
1007 pool_cache.get(&pool_hash).cloned()
1008 };
1009
1010 let payment_info = if let Some(info) = cached_info {
1011 debug!("Pool cache hit for hash {}", hex::encode(pool_hash));
1012 info
1013 } else {
1014 let info =
1016 payment_vault::get_completed_merkle_payment(&self.config.evm.network, pool_hash)
1017 .await
1018 .map_err(|e| {
1019 let pool_hex = hex::encode(pool_hash);
1020 Error::Payment(format!(
1021 "Failed to query merkle payment info for pool {pool_hex}: {e}"
1022 ))
1023 })?;
1024
1025 let paid_node_addresses: Vec<_> = info
1026 .paidNodeAddresses
1027 .iter()
1028 .map(|pna| (pna.rewardsAddress, usize::from(pna.poolIndex), pna.amount))
1029 .collect();
1030
1031 let on_chain_info = OnChainPaymentInfo {
1032 depth: info.depth,
1033 merkle_payment_timestamp: info.merklePaymentTimestamp,
1034 paid_node_addresses,
1035 };
1036
1037 {
1039 let mut pool_cache = self.pool_cache.lock();
1040 pool_cache.put(pool_hash, on_chain_info.clone());
1041 }
1042
1043 debug!(
1044 "Queried on-chain merkle payment info for pool {}: depth={}, timestamp={}, paid_nodes={}",
1045 hex::encode(pool_hash),
1046 on_chain_info.depth,
1047 on_chain_info.merkle_payment_timestamp,
1048 on_chain_info.paid_node_addresses.len()
1049 );
1050
1051 on_chain_info
1052 };
1053
1054 for candidate in &merkle_proof.winner_pool.candidate_nodes {
1056 if candidate.merkle_payment_timestamp != payment_info.merkle_payment_timestamp {
1057 return Err(Error::Payment(format!(
1058 "Candidate timestamp mismatch: expected {}, got {} (reward: {})",
1059 payment_info.merkle_payment_timestamp,
1060 candidate.merkle_payment_timestamp,
1061 candidate.reward_address
1062 )));
1063 }
1064 }
1065
1066 let smart_contract_root = merkle_proof.winner_pool.midpoint_proof.root();
1068
1069 evmlib::merkle_payments::verify_merkle_proof(
1072 &merkle_proof.address,
1073 &merkle_proof.data_proof,
1074 &merkle_proof.winner_pool.midpoint_proof,
1075 payment_info.depth,
1076 smart_contract_root,
1077 payment_info.merkle_payment_timestamp,
1078 )
1079 .map_err(|e| {
1080 let xorname_hex = hex::encode(xorname);
1081 Error::Payment(format!(
1082 "Merkle proof verification failed for {xorname_hex}: {e}"
1083 ))
1084 })?;
1085
1086 let expected_depth = payment_info.depth as usize;
1088 let actual_paid = payment_info.paid_node_addresses.len();
1089 if actual_paid != expected_depth {
1090 return Err(Error::Payment(format!(
1091 "Wrong number of paid nodes: expected {expected_depth}, got {actual_paid}"
1092 )));
1093 }
1094
1095 let expected_per_node = if payment_info.depth > 0 {
1099 let mut candidate_prices: Vec<Amount> = merkle_proof
1100 .winner_pool
1101 .candidate_nodes
1102 .iter()
1103 .map(|c| c.price)
1104 .collect();
1105 candidate_prices.sort_unstable(); let median_price = *candidate_prices
1108 .get(candidate_prices.len() / 2)
1109 .ok_or_else(|| Error::Payment("empty candidate pool in merkle proof".into()))?;
1110 let shift = u32::from(payment_info.depth);
1111 let multiplier = 1u64
1112 .checked_shl(shift)
1113 .ok_or_else(|| Error::Payment("merkle proof depth too large".into()))?;
1114 let total_amount = median_price * Amount::from(multiplier);
1115 total_amount / Amount::from(u64::from(payment_info.depth))
1116 } else {
1117 Amount::ZERO
1118 };
1119
1120 for (addr, idx, paid_amount) in &payment_info.paid_node_addresses {
1136 let node = merkle_proof
1137 .winner_pool
1138 .candidate_nodes
1139 .get(*idx)
1140 .ok_or_else(|| {
1141 Error::Payment(format!(
1142 "Paid node index {idx} out of bounds for pool size {}",
1143 merkle_proof.winner_pool.candidate_nodes.len()
1144 ))
1145 })?;
1146 if node.reward_address != *addr {
1147 return Err(Error::Payment(format!(
1148 "Paid node address mismatch at index {idx}: expected {addr}, got {}",
1149 node.reward_address
1150 )));
1151 }
1152 if *paid_amount < expected_per_node {
1153 return Err(Error::Payment(format!(
1154 "Underpayment for node at index {idx}: paid {paid_amount}, \
1155 expected at least {expected_per_node} \
1156 (median16 formula, depth={})",
1157 payment_info.depth
1158 )));
1159 }
1160 }
1161
1162 if crate::logging::enabled!(crate::logging::Level::INFO) {
1163 info!(
1164 "Merkle payment verified for {} (pool: {})",
1165 hex::encode(xorname),
1166 hex::encode(pool_hash)
1167 );
1168 }
1169
1170 Ok(())
1171 }
1172
1173 fn validate_local_recipient(&self, payment: &ProofOfPayment) -> Result<()> {
1175 let local_addr = &self.config.local_rewards_address;
1176 let is_recipient = payment
1177 .peer_quotes
1178 .iter()
1179 .any(|(_, quote)| quote.rewards_address == *local_addr);
1180 if !is_recipient {
1181 return Err(Error::Payment(
1182 "Payment proof does not include this node as a recipient".to_string(),
1183 ));
1184 }
1185 Ok(())
1186 }
1187}
1188
1189#[cfg(test)]
1190#[allow(clippy::expect_used)]
1191mod tests {
1192 use super::*;
1193 use evmlib::merkle_payments::MerklePaymentCandidatePool;
1194
1195 fn create_test_verifier() -> PaymentVerifier {
1198 let config = PaymentVerifierConfig {
1199 evm: EvmVerifierConfig::default(),
1200 cache_capacity: 100,
1201 local_rewards_address: RewardsAddress::new([1u8; 20]),
1202 };
1203 PaymentVerifier::new(config)
1204 }
1205
1206 #[test]
1207 fn test_payment_required_for_new_data() {
1208 let verifier = create_test_verifier();
1209 let xorname = [1u8; 32];
1210
1211 let status = verifier.check_payment_required(&xorname);
1213 assert_eq!(status, PaymentStatus::PaymentRequired);
1214 }
1215
1216 #[test]
1217 fn test_cache_hit() {
1218 let verifier = create_test_verifier();
1219 let xorname = [1u8; 32];
1220
1221 verifier.cache.insert(xorname);
1223
1224 let status = verifier.check_payment_required(&xorname);
1226 assert_eq!(status, PaymentStatus::CachedAsVerified);
1227 }
1228
1229 #[tokio::test]
1230 async fn test_verify_payment_without_proof_rejected() {
1231 let verifier = create_test_verifier();
1232 let xorname = [1u8; 32];
1233
1234 let result = verifier.verify_payment(&xorname, None).await;
1236 assert!(
1237 result.is_err(),
1238 "Expected Err without proof, got: {result:?}"
1239 );
1240 }
1241
1242 #[tokio::test]
1243 async fn test_verify_payment_cached() {
1244 let verifier = create_test_verifier();
1245 let xorname = [1u8; 32];
1246
1247 verifier.cache.insert(xorname);
1249
1250 let result = verifier.verify_payment(&xorname, None).await;
1252 assert!(result.is_ok());
1253 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
1254 }
1255
1256 #[test]
1257 fn test_payment_status_can_store() {
1258 assert!(PaymentStatus::CachedAsVerified.can_store());
1259 assert!(PaymentStatus::PaymentVerified.can_store());
1260 assert!(!PaymentStatus::PaymentRequired.can_store());
1261 }
1262
1263 #[test]
1264 fn test_payment_status_is_cached() {
1265 assert!(PaymentStatus::CachedAsVerified.is_cached());
1266 assert!(!PaymentStatus::PaymentVerified.is_cached());
1267 assert!(!PaymentStatus::PaymentRequired.is_cached());
1268 }
1269
1270 #[tokio::test]
1271 async fn test_cache_preload_bypasses_evm() {
1272 let verifier = create_test_verifier();
1273 let xorname = [42u8; 32];
1274
1275 assert_eq!(
1277 verifier.check_payment_required(&xorname),
1278 PaymentStatus::PaymentRequired
1279 );
1280
1281 verifier.cache.insert(xorname);
1283
1284 assert_eq!(
1286 verifier.check_payment_required(&xorname),
1287 PaymentStatus::CachedAsVerified
1288 );
1289 }
1290
1291 #[tokio::test]
1292 async fn test_proof_too_small() {
1293 let verifier = create_test_verifier();
1294 let xorname = [1u8; 32];
1295
1296 let small_proof = vec![0u8; MIN_PAYMENT_PROOF_SIZE_BYTES - 1];
1298 let result = verifier.verify_payment(&xorname, Some(&small_proof)).await;
1299 assert!(result.is_err());
1300 let err_msg = format!("{}", result.expect_err("should fail"));
1301 assert!(
1302 err_msg.contains("too small"),
1303 "Error should mention 'too small': {err_msg}"
1304 );
1305 }
1306
1307 #[tokio::test]
1308 async fn test_proof_too_large() {
1309 let verifier = create_test_verifier();
1310 let xorname = [2u8; 32];
1311
1312 let large_proof = vec![0u8; MAX_PAYMENT_PROOF_SIZE_BYTES + 1];
1314 let result = verifier.verify_payment(&xorname, Some(&large_proof)).await;
1315 assert!(result.is_err());
1316 let err_msg = format!("{}", result.expect_err("should fail"));
1317 assert!(
1318 err_msg.contains("too large"),
1319 "Error should mention 'too large': {err_msg}"
1320 );
1321 }
1322
1323 #[tokio::test]
1324 async fn test_proof_at_min_boundary_unknown_tag() {
1325 let verifier = create_test_verifier();
1326 let xorname = [3u8; 32];
1327
1328 let boundary_proof = vec![0xFFu8; MIN_PAYMENT_PROOF_SIZE_BYTES];
1330 let result = verifier
1331 .verify_payment(&xorname, Some(&boundary_proof))
1332 .await;
1333 assert!(result.is_err());
1334 let err_msg = format!("{}", result.expect_err("should fail"));
1335 assert!(
1336 err_msg.contains("Unknown payment proof type tag"),
1337 "Error should mention unknown tag: {err_msg}"
1338 );
1339 }
1340
1341 #[tokio::test]
1342 async fn test_proof_at_max_boundary_unknown_tag() {
1343 let verifier = create_test_verifier();
1344 let xorname = [4u8; 32];
1345
1346 let boundary_proof = vec![0xFFu8; MAX_PAYMENT_PROOF_SIZE_BYTES];
1348 let result = verifier
1349 .verify_payment(&xorname, Some(&boundary_proof))
1350 .await;
1351 assert!(result.is_err());
1352 let err_msg = format!("{}", result.expect_err("should fail"));
1353 assert!(
1354 err_msg.contains("Unknown payment proof type tag"),
1355 "Error should mention unknown tag: {err_msg}"
1356 );
1357 }
1358
1359 #[tokio::test]
1360 async fn test_malformed_single_node_proof() {
1361 let verifier = create_test_verifier();
1362 let xorname = [5u8; 32];
1363
1364 let mut garbage = vec![crate::ant_protocol::PROOF_TAG_SINGLE_NODE];
1366 garbage.extend_from_slice(&[0xAB; 63]);
1367 let result = verifier.verify_payment(&xorname, Some(&garbage)).await;
1368 assert!(result.is_err());
1369 let err_msg = format!("{}", result.expect_err("should fail"));
1370 assert!(
1371 err_msg.contains("deserialize") || err_msg.contains("Failed"),
1372 "Error should mention deserialization failure: {err_msg}"
1373 );
1374 }
1375
1376 #[test]
1377 fn test_cache_len_getter() {
1378 let verifier = create_test_verifier();
1379 assert_eq!(verifier.cache_len(), 0);
1380
1381 verifier.cache.insert([10u8; 32]);
1382 assert_eq!(verifier.cache_len(), 1);
1383
1384 verifier.cache.insert([20u8; 32]);
1385 assert_eq!(verifier.cache_len(), 2);
1386 }
1387
1388 #[test]
1389 fn test_cache_stats_after_operations() {
1390 let verifier = create_test_verifier();
1391 let xorname = [7u8; 32];
1392
1393 verifier.check_payment_required(&xorname);
1395 let stats = verifier.cache_stats();
1396 assert_eq!(stats.misses, 1);
1397 assert_eq!(stats.hits, 0);
1398
1399 verifier.cache.insert(xorname);
1401 verifier.check_payment_required(&xorname);
1402 let stats = verifier.cache_stats();
1403 assert_eq!(stats.hits, 1);
1404 assert_eq!(stats.misses, 1);
1405 assert_eq!(stats.additions, 1);
1406 }
1407
1408 #[tokio::test]
1409 async fn test_concurrent_cache_lookups() {
1410 let verifier = std::sync::Arc::new(create_test_verifier());
1411
1412 for i in 0..10u8 {
1414 verifier.cache.insert([i; 32]);
1415 }
1416
1417 let mut handles = Vec::new();
1418 for i in 0..10u8 {
1419 let v = verifier.clone();
1420 handles.push(tokio::spawn(async move {
1421 let xorname = [i; 32];
1422 v.verify_payment(&xorname, None).await
1423 }));
1424 }
1425
1426 for handle in handles {
1427 let result = handle.await.expect("task panicked");
1428 assert!(result.is_ok());
1429 assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
1430 }
1431
1432 assert_eq!(verifier.cache_len(), 10);
1433 }
1434
1435 #[test]
1436 fn test_default_evm_config() {
1437 let _config = EvmVerifierConfig::default();
1438 }
1440
1441 #[test]
1442 fn test_real_ml_dsa_proof_size_within_limits() {
1443 use crate::payment::metrics::QuotingMetricsTracker;
1444 use crate::payment::proof::PaymentProof;
1445 use crate::payment::quote::{QuoteGenerator, XorName};
1446 use alloy::primitives::FixedBytes;
1447 use evmlib::{EncodedPeerId, RewardsAddress};
1448 use saorsa_core::MlDsa65;
1449 use saorsa_pqc::pqc::types::MlDsaSecretKey;
1450 use saorsa_pqc::pqc::MlDsaOperations;
1451
1452 let ml_dsa = MlDsa65::new();
1453 let mut peer_quotes = Vec::new();
1454
1455 for i in 0..5u8 {
1456 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
1457
1458 let rewards_address = RewardsAddress::new([i; 20]);
1459 let metrics_tracker = QuotingMetricsTracker::new(0);
1460 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
1461
1462 let pub_key_bytes = public_key.as_bytes().to_vec();
1463 let sk_bytes = secret_key.as_bytes().to_vec();
1464 generator.set_signer(pub_key_bytes, move |msg| {
1465 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
1466 let ml_dsa = MlDsa65::new();
1467 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
1468 });
1469
1470 let content: XorName = [i; 32];
1471 let quote = generator.create_quote(content, 4096, 0).expect("quote");
1472
1473 peer_quotes.push((EncodedPeerId::new(rand::random()), quote));
1474 }
1475
1476 let proof = PaymentProof {
1477 proof_of_payment: ProofOfPayment { peer_quotes },
1478 tx_hashes: vec![FixedBytes::from([0xABu8; 32])],
1479 };
1480
1481 let proof_bytes =
1482 crate::payment::proof::serialize_single_node_proof(&proof).expect("serialize");
1483
1484 assert!(
1487 proof_bytes.len() > 20_000,
1488 "Real 7-quote ML-DSA proof should be > 20 KB, got {} bytes",
1489 proof_bytes.len()
1490 );
1491 assert!(
1492 proof_bytes.len() < MAX_PAYMENT_PROOF_SIZE_BYTES,
1493 "Real 7-quote ML-DSA proof ({} bytes) should fit within {} byte limit",
1494 proof_bytes.len(),
1495 MAX_PAYMENT_PROOF_SIZE_BYTES
1496 );
1497 }
1498
1499 #[tokio::test]
1500 async fn test_content_address_mismatch_rejected() {
1501 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1502 use evmlib::{EncodedPeerId, PaymentQuote, RewardsAddress};
1503 use std::time::SystemTime;
1504
1505 let verifier = create_test_verifier();
1506
1507 let target_xorname = [0xAAu8; 32];
1509
1510 let wrong_xorname = [0xBBu8; 32];
1512 let quote = PaymentQuote {
1513 content: xor_name::XorName(wrong_xorname),
1514 timestamp: SystemTime::now(),
1515 price: Amount::from(1u64),
1516 rewards_address: RewardsAddress::new([1u8; 20]),
1517 pub_key: vec![0u8; 64],
1518 signature: vec![0u8; 64],
1519 };
1520
1521 let mut peer_quotes = Vec::new();
1523 for _ in 0..CLOSE_GROUP_SIZE {
1524 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1525 }
1526
1527 let proof = PaymentProof {
1528 proof_of_payment: ProofOfPayment { peer_quotes },
1529 tx_hashes: vec![],
1530 };
1531
1532 let proof_bytes = serialize_single_node_proof(&proof).expect("serialize proof");
1533
1534 let result = verifier
1535 .verify_payment(&target_xorname, Some(&proof_bytes))
1536 .await;
1537
1538 assert!(result.is_err(), "Should reject mismatched content address");
1539 let err_msg = format!("{}", result.expect_err("should be error"));
1540 assert!(
1541 err_msg.contains("content address mismatch"),
1542 "Error should mention 'content address mismatch': {err_msg}"
1543 );
1544 }
1545
1546 fn make_fake_quote(
1548 xorname: [u8; 32],
1549 timestamp: SystemTime,
1550 rewards_address: RewardsAddress,
1551 ) -> evmlib::PaymentQuote {
1552 use evmlib::PaymentQuote;
1553
1554 PaymentQuote {
1555 content: xor_name::XorName(xorname),
1556 timestamp,
1557 price: Amount::from(1u64),
1558 rewards_address,
1559 pub_key: vec![0u8; 64],
1560 signature: vec![0u8; 64],
1561 }
1562 }
1563
1564 fn serialize_proof(peer_quotes: Vec<(evmlib::EncodedPeerId, evmlib::PaymentQuote)>) -> Vec<u8> {
1566 use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1567
1568 let proof = PaymentProof {
1569 proof_of_payment: ProofOfPayment { peer_quotes },
1570 tx_hashes: vec![],
1571 };
1572 serialize_single_node_proof(&proof).expect("serialize proof")
1573 }
1574
1575 #[tokio::test]
1576 async fn test_expired_quote_rejected() {
1577 use evmlib::{EncodedPeerId, RewardsAddress};
1578 use std::time::Duration;
1579
1580 let verifier = create_test_verifier();
1581 let xorname = [0xCCu8; 32];
1582 let rewards_addr = RewardsAddress::new([1u8; 20]);
1583
1584 let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
1586 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1587
1588 let mut peer_quotes = Vec::new();
1589 for _ in 0..CLOSE_GROUP_SIZE {
1590 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1591 }
1592
1593 let proof_bytes = serialize_proof(peer_quotes);
1594 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1595
1596 assert!(result.is_err(), "Should reject expired quote");
1597 let err_msg = format!("{}", result.expect_err("should fail"));
1598 assert!(
1599 err_msg.contains("expired"),
1600 "Error should mention 'expired': {err_msg}"
1601 );
1602 }
1603
1604 #[tokio::test]
1605 async fn test_future_timestamp_rejected() {
1606 use evmlib::{EncodedPeerId, RewardsAddress};
1607 use std::time::Duration;
1608
1609 let verifier = create_test_verifier();
1610 let xorname = [0xDDu8; 32];
1611 let rewards_addr = RewardsAddress::new([1u8; 20]);
1612
1613 let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
1615 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1616
1617 let mut peer_quotes = Vec::new();
1618 for _ in 0..CLOSE_GROUP_SIZE {
1619 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1620 }
1621
1622 let proof_bytes = serialize_proof(peer_quotes);
1623 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1624
1625 assert!(result.is_err(), "Should reject future-timestamped quote");
1626 let err_msg = format!("{}", result.expect_err("should fail"));
1627 assert!(
1628 err_msg.contains("future"),
1629 "Error should mention 'future': {err_msg}"
1630 );
1631 }
1632
1633 #[tokio::test]
1634 async fn test_quote_within_clock_skew_tolerance_accepted() {
1635 use evmlib::{EncodedPeerId, RewardsAddress};
1636 use std::time::Duration;
1637
1638 let verifier = create_test_verifier();
1639 let xorname = [0xD1u8; 32];
1640 let rewards_addr = RewardsAddress::new([1u8; 20]);
1641
1642 let future_timestamp = SystemTime::now() + Duration::from_secs(30);
1644 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1645
1646 let mut peer_quotes = Vec::new();
1647 for _ in 0..CLOSE_GROUP_SIZE {
1648 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1649 }
1650
1651 let proof_bytes = serialize_proof(peer_quotes);
1652 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1653
1654 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1656 assert!(
1657 !err_msg.contains("future"),
1658 "Should pass timestamp check (within tolerance), but got: {err_msg}"
1659 );
1660 }
1661
1662 #[tokio::test]
1663 async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
1664 use evmlib::{EncodedPeerId, RewardsAddress};
1665 use std::time::Duration;
1666
1667 let verifier = create_test_verifier();
1668 let xorname = [0xD2u8; 32];
1669 let rewards_addr = RewardsAddress::new([1u8; 20]);
1670
1671 let future_timestamp = SystemTime::now() + Duration::from_secs(120);
1673 let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1674
1675 let mut peer_quotes = Vec::new();
1676 for _ in 0..CLOSE_GROUP_SIZE {
1677 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1678 }
1679
1680 let proof_bytes = serialize_proof(peer_quotes);
1681 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1682
1683 assert!(
1684 result.is_err(),
1685 "Should reject quote beyond clock skew tolerance"
1686 );
1687 let err_msg = format!("{}", result.expect_err("should fail"));
1688 assert!(
1689 err_msg.contains("future"),
1690 "Error should mention 'future': {err_msg}"
1691 );
1692 }
1693
1694 #[tokio::test]
1695 async fn test_quote_23h_old_still_accepted() {
1696 use evmlib::{EncodedPeerId, RewardsAddress};
1697 use std::time::Duration;
1698
1699 let verifier = create_test_verifier();
1700 let xorname = [0xD3u8; 32];
1701 let rewards_addr = RewardsAddress::new([1u8; 20]);
1702
1703 let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
1705 let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1706
1707 let mut peer_quotes = Vec::new();
1708 for _ in 0..CLOSE_GROUP_SIZE {
1709 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1710 }
1711
1712 let proof_bytes = serialize_proof(peer_quotes);
1713 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1714
1715 let err_msg = format!("{}", result.expect_err("should fail at later check"));
1717 assert!(
1718 !err_msg.contains("expired"),
1719 "Should pass expiry check (23h < 24h), but got: {err_msg}"
1720 );
1721 }
1722
1723 fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> evmlib::EncodedPeerId {
1725 let ant_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1726 evmlib::EncodedPeerId::new(*ant_peer_id.as_bytes())
1727 }
1728
1729 #[tokio::test]
1730 async fn test_local_not_in_paid_set_rejected() {
1731 use evmlib::RewardsAddress;
1732 use saorsa_core::MlDsa65;
1733 use saorsa_pqc::pqc::MlDsaOperations;
1734
1735 let local_addr = RewardsAddress::new([0xAAu8; 20]);
1737 let config = PaymentVerifierConfig {
1738 evm: EvmVerifierConfig {
1739 network: EvmNetwork::ArbitrumOne,
1740 },
1741 cache_capacity: 100,
1742 local_rewards_address: local_addr,
1743 };
1744 let verifier = PaymentVerifier::new(config);
1745
1746 let xorname = [0xEEu8; 32];
1747 let other_addr = RewardsAddress::new([0xBBu8; 20]);
1749
1750 let ml_dsa = MlDsa65::new();
1752 let mut peer_quotes = Vec::new();
1753 for _ in 0..CLOSE_GROUP_SIZE {
1754 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1755 let pub_key_bytes = public_key.as_bytes().to_vec();
1756 let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1757
1758 let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1759 quote.pub_key = pub_key_bytes;
1760
1761 peer_quotes.push((encoded, quote));
1762 }
1763
1764 let proof_bytes = serialize_proof(peer_quotes);
1765 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1766
1767 assert!(result.is_err(), "Should reject payment not addressed to us");
1768 let err_msg = format!("{}", result.expect_err("should fail"));
1769 assert!(
1770 err_msg.contains("does not include this node as a recipient"),
1771 "Error should mention recipient rejection: {err_msg}"
1772 );
1773 }
1774
1775 #[tokio::test]
1776 async fn test_wrong_peer_binding_rejected() {
1777 use evmlib::{EncodedPeerId, RewardsAddress};
1778 use saorsa_core::MlDsa65;
1779 use saorsa_pqc::pqc::MlDsaOperations;
1780
1781 let verifier = create_test_verifier();
1782 let xorname = [0xFFu8; 32];
1783 let rewards_addr = RewardsAddress::new([1u8; 20]);
1784
1785 let ml_dsa = MlDsa65::new();
1787 let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1788 let pub_key_bytes = public_key.as_bytes().to_vec();
1789
1790 let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1793 quote.pub_key = pub_key_bytes;
1794
1795 let mut peer_quotes = Vec::new();
1797 for _ in 0..CLOSE_GROUP_SIZE {
1798 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1799 }
1800
1801 let proof_bytes = serialize_proof(peer_quotes);
1802 let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1803
1804 assert!(result.is_err(), "Should reject wrong peer binding");
1805 let err_msg = format!("{}", result.expect_err("should fail"));
1806 assert!(
1807 err_msg.contains("pub_key does not belong to claimed peer"),
1808 "Error should mention binding mismatch: {err_msg}"
1809 );
1810 }
1811
1812 #[tokio::test]
1817 async fn test_merkle_tagged_proof_invalid_data_rejected() {
1818 use crate::ant_protocol::PROOF_TAG_MERKLE;
1819
1820 let verifier = create_test_verifier();
1821 let xorname = [0xA1u8; 32];
1822
1823 let mut merkle_garbage = Vec::with_capacity(64);
1826 merkle_garbage.push(PROOF_TAG_MERKLE);
1827 merkle_garbage.extend_from_slice(&[0xAB; 63]);
1828
1829 let result = verifier
1830 .verify_payment(&xorname, Some(&merkle_garbage))
1831 .await;
1832
1833 assert!(
1834 result.is_err(),
1835 "Should reject merkle proof with invalid body"
1836 );
1837 let err_msg = format!("{}", result.expect_err("should fail"));
1838 assert!(
1839 err_msg.contains("deserialize") || err_msg.contains("merkle proof"),
1840 "Error should mention deserialization failure: {err_msg}"
1841 );
1842 }
1843
1844 #[tokio::test]
1845 async fn test_single_node_tagged_proof_deserialization() {
1846 use crate::payment::proof::serialize_single_node_proof;
1847 use evmlib::{EncodedPeerId, RewardsAddress};
1848
1849 let verifier = create_test_verifier();
1850 let xorname = [0xA2u8; 32];
1851 let rewards_addr = RewardsAddress::new([1u8; 20]);
1852
1853 let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1855 let mut peer_quotes = Vec::new();
1856 for _ in 0..CLOSE_GROUP_SIZE {
1857 peer_quotes.push((EncodedPeerId::new(rand::random()), quote.clone()));
1858 }
1859
1860 let proof = crate::payment::proof::PaymentProof {
1861 proof_of_payment: ProofOfPayment {
1862 peer_quotes: peer_quotes.clone(),
1863 },
1864 tx_hashes: vec![],
1865 };
1866
1867 let tagged_bytes = serialize_single_node_proof(&proof).expect("serialize tagged proof");
1868
1869 assert_eq!(
1871 crate::payment::proof::detect_proof_type(&tagged_bytes),
1872 Some(crate::payment::proof::ProofType::SingleNode)
1873 );
1874
1875 let result = verifier.verify_payment(&xorname, Some(&tagged_bytes)).await;
1879
1880 assert!(result.is_err(), "Should fail at quote validation stage");
1881 let err_msg = format!("{}", result.expect_err("should fail"));
1882 assert!(
1884 !err_msg.contains("deserialize"),
1885 "Should pass deserialization but fail later: {err_msg}"
1886 );
1887 }
1888
1889 #[test]
1890 fn test_pool_cache_insert_and_lookup() {
1891 use evmlib::merkle_batch_payment::PoolHash;
1892
1893 let verifier = create_test_verifier();
1896
1897 let pool_hash: PoolHash = [0xBBu8; 32];
1898 let payment_info = evmlib::merkle_payments::OnChainPaymentInfo {
1899 depth: 4,
1900 merkle_payment_timestamp: 1_700_000_000,
1901 paid_node_addresses: vec![],
1902 };
1903
1904 {
1906 let mut cache = verifier.pool_cache.lock();
1907 cache.put(pool_hash, payment_info);
1908 }
1909
1910 {
1912 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1913 assert!(found.is_some(), "Pool hash should be in cache after insert");
1914 let info = found.expect("cached info");
1915 assert_eq!(info.depth, 4);
1916 assert_eq!(info.merkle_payment_timestamp, 1_700_000_000);
1917 }
1918
1919 {
1921 let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1922 assert!(
1923 found.is_some(),
1924 "Pool hash should still be in cache on second lookup"
1925 );
1926 }
1927
1928 let other_hash: PoolHash = [0xCCu8; 32];
1930 {
1931 let found = verifier.pool_cache.lock().get(&other_hash).cloned();
1932 assert!(found.is_none(), "Unknown pool hash should not be in cache");
1933 }
1934 }
1935
1936 #[tokio::test]
1937 async fn closeness_pass_cache_short_circuits_second_call() {
1938 let verifier = create_test_verifier();
1944 let pool_hash = [0xAAu8; 32];
1945 verifier.closeness_pass_cache.lock().put(pool_hash, ());
1946
1947 let pool = MerklePaymentCandidatePool {
1950 midpoint_proof: fake_midpoint_proof(),
1951 candidate_nodes: make_candidate_nodes(1_700_000_000),
1952 };
1953
1954 let result = verifier
1955 .verify_merkle_candidate_closeness(&pool, pool_hash)
1956 .await;
1957 assert!(
1958 result.is_ok(),
1959 "cached pool hash must bypass the inner check and return Ok(()), got: {result:?}"
1960 );
1961 }
1962
1963 #[tokio::test]
1964 async fn closeness_single_flight_concurrent_readers_share_one_verification() {
1965 let verifier = Arc::new(create_test_verifier());
1971 let pool_hash = [0x77u8; 32];
1972 let pool = MerklePaymentCandidatePool {
1973 midpoint_proof: fake_midpoint_proof(),
1974 candidate_nodes: make_candidate_nodes(1_700_000_000),
1975 };
1976
1977 let v1 = Arc::clone(&verifier);
1978 let p1 = pool.clone();
1979 let v2 = Arc::clone(&verifier);
1980 let p2 = pool.clone();
1981
1982 let (r1, r2) = tokio::join!(
1983 async move { v1.verify_merkle_candidate_closeness(&p1, pool_hash).await },
1984 async move { v2.verify_merkle_candidate_closeness(&p2, pool_hash).await },
1985 );
1986
1987 assert_eq!(r1.is_ok(), r2.is_ok(), "concurrent callers must agree");
1988 assert!(
1989 r1.is_ok(),
1990 "both callers must succeed on the test-utils path"
1991 );
1992 assert!(
1993 verifier
1994 .closeness_pass_cache
1995 .lock()
1996 .get(&pool_hash)
1997 .is_some(),
1998 "success path must populate the pass cache"
1999 );
2000 assert!(
2001 verifier.inflight_closeness.lock().get(&pool_hash).is_none(),
2002 "inflight slot must be cleared after the leader finishes"
2003 );
2004 }
2005
2006 #[tokio::test]
2007 async fn closeness_waiter_reads_leaders_published_failure() {
2008 let verifier = Arc::new(create_test_verifier());
2014 let pool_hash = [0x55u8; 32];
2015 let slot = Arc::new(ClosenessSlot::new());
2016 verifier
2017 .inflight_closeness
2018 .lock()
2019 .put(pool_hash, Arc::clone(&slot));
2020
2021 let pool = MerklePaymentCandidatePool {
2022 midpoint_proof: fake_midpoint_proof(),
2023 candidate_nodes: make_candidate_nodes(1_700_000_000),
2024 };
2025
2026 let verifier_c = Arc::clone(&verifier);
2027 let pool_c = pool.clone();
2028 let waiter = tokio::spawn(async move {
2029 verifier_c
2030 .verify_merkle_candidate_closeness(&pool_c, pool_hash)
2031 .await
2032 });
2033
2034 for _ in 0..5 {
2038 tokio::task::yield_now().await;
2039 }
2040
2041 slot.result
2044 .set(Err("forged pool: not close enough".to_string()))
2045 .expect("set once");
2046 verifier.inflight_closeness.lock().pop(&pool_hash);
2047 slot.notify.notify_waiters();
2048
2049 let result = waiter.await.expect("task panicked");
2050 let err = result.expect_err("waiter must return the leader's published failure");
2051 assert!(
2052 err.to_string().contains("forged pool"),
2053 "waiter must surface the leader's error message, got: {err}"
2054 );
2055 }
2056
2057 #[tokio::test]
2058 async fn closeness_rejects_pool_with_duplicate_candidate_pub_keys() {
2059 let verifier = create_test_verifier();
2066 let pool_hash = [0xDDu8; 32];
2067
2068 let mut candidates = make_candidate_nodes(1_700_000_000);
2071 let shared_pub_key = candidates
2072 .first()
2073 .expect("make_candidate_nodes returns CANDIDATES_PER_POOL entries")
2074 .pub_key
2075 .clone();
2076 for c in &mut candidates {
2077 c.pub_key = shared_pub_key.clone();
2078 }
2079 let pool = MerklePaymentCandidatePool {
2080 midpoint_proof: fake_midpoint_proof(),
2081 candidate_nodes: candidates,
2082 };
2083
2084 let result = verifier
2085 .verify_merkle_candidate_closeness(&pool, pool_hash)
2086 .await;
2087 let err = result.expect_err("duplicate candidate PeerIds must be rejected");
2088 let msg = err.to_string();
2089 assert!(
2090 msg.contains("duplicate candidate PeerId"),
2091 "rejection must be the duplicate-PeerId branch, got: {msg}"
2092 );
2093 }
2094
2095 fn fake_midpoint_proof() -> evmlib::merkle_payments::MidpointProof {
2101 let leaves = vec![xor_name::XorName([1u8; 32]), xor_name::XorName([2u8; 32])];
2103 let tree = evmlib::merkle_payments::MerkleTree::from_xornames(leaves).expect("tree");
2104 let candidates = tree.reward_candidates(1_700_000_000).expect("candidates");
2105 candidates.first().expect("at least one").clone()
2106 }
2107
2108 fn make_candidate_nodes(
2114 timestamp: u64,
2115 ) -> [evmlib::merkle_payments::MerklePaymentCandidateNode;
2116 evmlib::merkle_payments::CANDIDATES_PER_POOL] {
2117 use evmlib::merkle_payments::{MerklePaymentCandidateNode, CANDIDATES_PER_POOL};
2118 use saorsa_core::MlDsa65;
2119 use saorsa_pqc::pqc::types::MlDsaSecretKey;
2120 use saorsa_pqc::pqc::MlDsaOperations;
2121
2122 std::array::from_fn::<_, CANDIDATES_PER_POOL, _>(|i| {
2123 let ml_dsa = MlDsa65::new();
2124 let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
2125 let price = evmlib::common::Amount::from(1024u64);
2126 #[allow(clippy::cast_possible_truncation)]
2127 let reward_address = RewardsAddress::new([i as u8; 20]);
2128 let msg = MerklePaymentCandidateNode::bytes_to_sign(&price, &reward_address, timestamp);
2129 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
2130 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
2131
2132 MerklePaymentCandidateNode {
2133 pub_key: pub_key.as_bytes().to_vec(),
2134 price,
2135 reward_address,
2136 merkle_payment_timestamp: timestamp,
2137 signature,
2138 }
2139 })
2140 }
2141
2142 fn make_valid_merkle_proof() -> (
2145 evmlib::merkle_payments::MerklePaymentProof,
2146 evmlib::merkle_batch_payment::PoolHash,
2147 [u8; 32],
2148 u64,
2149 ) {
2150 use evmlib::merkle_payments::{MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree};
2151
2152 let timestamp = std::time::SystemTime::now()
2153 .duration_since(std::time::UNIX_EPOCH)
2154 .expect("system time")
2155 .as_secs();
2156
2157 let addresses: Vec<xor_name::XorName> = (0..4u8)
2158 .map(|i| xor_name::XorName::from_content(&[i]))
2159 .collect();
2160 let tree = MerkleTree::from_xornames(addresses.clone()).expect("tree");
2161
2162 let candidate_nodes = make_candidate_nodes(timestamp);
2163
2164 let reward_candidates = tree
2165 .reward_candidates(timestamp)
2166 .expect("reward candidates");
2167 let midpoint_proof = reward_candidates
2168 .first()
2169 .expect("at least one candidate")
2170 .clone();
2171
2172 let pool = MerklePaymentCandidatePool {
2173 midpoint_proof,
2174 candidate_nodes,
2175 };
2176
2177 let first_address = *addresses.first().expect("first address");
2178 let address_proof = tree
2179 .generate_address_proof(0, first_address)
2180 .expect("proof");
2181
2182 let merkle_proof = MerklePaymentProof::new(first_address, address_proof, pool);
2183 let pool_hash = merkle_proof.winner_pool_hash();
2184 let xorname = first_address.0;
2185
2186 (merkle_proof, pool_hash, xorname, timestamp)
2187 }
2188
2189 fn make_valid_merkle_proof_bytes() -> (
2192 [u8; 32],
2193 Vec<u8>,
2194 evmlib::merkle_batch_payment::PoolHash,
2195 u64,
2196 ) {
2197 let (merkle_proof, pool_hash, xorname, timestamp) = make_valid_merkle_proof();
2198 let tagged = crate::payment::proof::serialize_merkle_proof(&merkle_proof)
2199 .expect("serialize merkle proof");
2200 (xorname, tagged, pool_hash, timestamp)
2201 }
2202
2203 #[tokio::test]
2204 async fn test_merkle_address_mismatch_rejected() {
2205 let verifier = create_test_verifier();
2206 let (_correct_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
2207
2208 let wrong_xorname = [0xFFu8; 32];
2210
2211 let result = verifier
2212 .verify_payment(&wrong_xorname, Some(&tagged_proof))
2213 .await;
2214
2215 assert!(
2216 result.is_err(),
2217 "Should reject merkle proof address mismatch"
2218 );
2219 let err_msg = format!("{}", result.expect_err("should fail"));
2220 assert!(
2221 err_msg.contains("address mismatch") || err_msg.contains("Merkle proof address"),
2222 "Error should mention address mismatch: {err_msg}"
2223 );
2224 }
2225
2226 #[tokio::test]
2227 async fn test_merkle_malformed_body_rejected() {
2228 let verifier = create_test_verifier();
2229 let xorname = [0xA3u8; 32];
2230
2231 let mut bad_proof = vec![crate::ant_protocol::PROOF_TAG_MERKLE];
2233 bad_proof.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
2234 bad_proof.extend_from_slice(&[0x00; 10]);
2235 while bad_proof.len() < MIN_PAYMENT_PROOF_SIZE_BYTES {
2237 bad_proof.push(0x00);
2238 }
2239
2240 let result = verifier.verify_payment(&xorname, Some(&bad_proof)).await;
2241
2242 assert!(result.is_err(), "Should reject malformed merkle body");
2243 let err_msg = format!("{}", result.expect_err("should fail"));
2244 assert!(
2245 err_msg.contains("deserialize") || err_msg.contains("Failed"),
2246 "Error should mention deserialization: {err_msg}"
2247 );
2248 }
2249
2250 #[test]
2251 fn test_merkle_proof_serialized_size_within_limits() {
2252 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
2253
2254 assert!(
2256 tagged_proof.len() >= MIN_PAYMENT_PROOF_SIZE_BYTES,
2257 "Merkle proof ({} bytes) should be >= min {} bytes",
2258 tagged_proof.len(),
2259 MIN_PAYMENT_PROOF_SIZE_BYTES
2260 );
2261 assert!(
2262 tagged_proof.len() <= MAX_PAYMENT_PROOF_SIZE_BYTES,
2263 "Merkle proof ({} bytes) should be <= max {} bytes",
2264 tagged_proof.len(),
2265 MAX_PAYMENT_PROOF_SIZE_BYTES
2266 );
2267 }
2268
2269 #[test]
2270 fn test_merkle_proof_tag_is_correct() {
2271 let (_xorname, tagged_proof, _pool_hash, _ts) = make_valid_merkle_proof_bytes();
2272
2273 assert_eq!(
2274 tagged_proof.first().copied(),
2275 Some(crate::ant_protocol::PROOF_TAG_MERKLE),
2276 "First byte must be the merkle tag"
2277 );
2278 assert_eq!(
2279 crate::payment::proof::detect_proof_type(&tagged_proof),
2280 Some(crate::payment::proof::ProofType::Merkle)
2281 );
2282 }
2283
2284 #[test]
2285 fn test_pool_cache_eviction() {
2286 use evmlib::merkle_batch_payment::PoolHash;
2287
2288 let config = PaymentVerifierConfig {
2289 evm: EvmVerifierConfig::default(),
2290 cache_capacity: 100,
2291 local_rewards_address: RewardsAddress::new([1u8; 20]),
2292 };
2293 let verifier = PaymentVerifier::new(config);
2294
2295 for i in 0..DEFAULT_POOL_CACHE_CAPACITY {
2297 let mut hash: PoolHash = [0u8; 32];
2298 let idx_bytes = i.to_le_bytes();
2300 for (j, b) in idx_bytes.iter().enumerate() {
2301 if j < 32 {
2302 hash[j] = *b;
2303 }
2304 }
2305 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2306 depth: 4,
2307 merkle_payment_timestamp: 1_700_000_000,
2308 paid_node_addresses: vec![],
2309 };
2310 verifier.pool_cache.lock().put(hash, info);
2311 }
2312
2313 assert_eq!(
2314 verifier.pool_cache.lock().len(),
2315 DEFAULT_POOL_CACHE_CAPACITY
2316 );
2317
2318 let overflow_hash: PoolHash = [0xFFu8; 32];
2320 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2321 depth: 8,
2322 merkle_payment_timestamp: 1_800_000_000,
2323 paid_node_addresses: vec![],
2324 };
2325 verifier.pool_cache.lock().put(overflow_hash, info);
2326
2327 assert_eq!(
2329 verifier.pool_cache.lock().len(),
2330 DEFAULT_POOL_CACHE_CAPACITY
2331 );
2332
2333 let found = verifier.pool_cache.lock().get(&overflow_hash).cloned();
2335 assert!(
2336 found.is_some(),
2337 "Newly inserted pool hash should be present"
2338 );
2339 assert_eq!(found.expect("info").depth, 8);
2340 }
2341
2342 #[test]
2343 fn test_pool_cache_concurrent_access() {
2344 use evmlib::merkle_batch_payment::PoolHash;
2345 use std::sync::Arc;
2346
2347 let verifier = Arc::new(create_test_verifier());
2348
2349 let mut handles = Vec::new();
2350 for i in 0..20u8 {
2351 let v = verifier.clone();
2352 handles.push(std::thread::spawn(move || {
2353 let hash: PoolHash = [i; 32];
2354 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2355 depth: i,
2356 merkle_payment_timestamp: u64::from(i) * 1000,
2357 paid_node_addresses: vec![],
2358 };
2359 v.pool_cache.lock().put(hash, info);
2360
2361 let found = v.pool_cache.lock().get(&hash).cloned();
2363 assert!(found.is_some(), "Entry {i} should be readable after insert");
2364 }));
2365 }
2366
2367 for handle in handles {
2368 handle.join().expect("thread panicked");
2369 }
2370
2371 assert_eq!(verifier.pool_cache.lock().len(), 20);
2373 }
2374
2375 #[tokio::test]
2376 async fn test_merkle_tampered_candidate_signature_rejected() {
2377 let verifier = create_test_verifier();
2378
2379 let (mut merkle_proof, _pool_hash, xorname, timestamp) = make_valid_merkle_proof();
2380
2381 if let Some(byte) = merkle_proof
2383 .winner_pool
2384 .candidate_nodes
2385 .first_mut()
2386 .and_then(|c| c.signature.first_mut())
2387 {
2388 *byte ^= 0xFF;
2389 }
2390
2391 let tampered_pool_hash = merkle_proof.winner_pool_hash();
2393
2394 {
2396 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2397 depth: 4,
2398 merkle_payment_timestamp: timestamp,
2399 paid_node_addresses: vec![],
2400 };
2401 verifier.pool_cache.lock().put(tampered_pool_hash, info);
2402 }
2403
2404 let tagged =
2405 crate::payment::proof::serialize_merkle_proof(&merkle_proof).expect("serialize");
2406
2407 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
2408
2409 assert!(
2410 result.is_err(),
2411 "Should reject merkle proof with tampered candidate signature"
2412 );
2413 let err_msg = format!("{}", result.expect_err("should fail"));
2414 assert!(
2415 err_msg.contains("Invalid ML-DSA-65 signature"),
2416 "Error should mention invalid signature: {err_msg}"
2417 );
2418 }
2419
2420 #[tokio::test]
2421 async fn test_merkle_timestamp_mismatch_rejected() {
2422 let verifier = create_test_verifier();
2423
2424 let (xorname, tagged, pool_hash, timestamp) = make_valid_merkle_proof_bytes();
2425
2426 {
2428 let mismatched_ts = timestamp + 9999;
2429 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2430 depth: 4,
2431 merkle_payment_timestamp: mismatched_ts,
2432 paid_node_addresses: vec![],
2433 };
2434 verifier.pool_cache.lock().put(pool_hash, info);
2435 }
2436
2437 let result = verifier.verify_payment(&xorname, Some(&tagged)).await;
2438
2439 assert!(
2440 result.is_err(),
2441 "Should reject merkle proof with timestamp mismatch"
2442 );
2443 let err_msg = format!("{}", result.expect_err("should fail"));
2444 assert!(
2445 err_msg.contains("timestamp mismatch"),
2446 "Error should mention timestamp mismatch: {err_msg}"
2447 );
2448 }
2449
2450 #[tokio::test]
2451 async fn test_merkle_paid_node_index_out_of_bounds_rejected() {
2452 let verifier = create_test_verifier();
2453 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
2454
2455 {
2459 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2460 depth: 2,
2461 merkle_payment_timestamp: ts,
2462 paid_node_addresses: vec![
2463 (RewardsAddress::new([0u8; 20]), 0, Amount::from(2048u64)),
2466 (RewardsAddress::new([1u8; 20]), 999, Amount::from(2048u64)),
2468 ],
2469 };
2470 verifier.pool_cache.lock().put(pool_hash, info);
2471 }
2472
2473 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
2474
2475 assert!(
2476 result.is_err(),
2477 "Should reject paid node index out of bounds"
2478 );
2479 let err_msg = format!("{}", result.expect_err("should fail"));
2480 assert!(
2481 err_msg.contains("out of bounds"),
2482 "Error should mention out of bounds: {err_msg}"
2483 );
2484 }
2485
2486 #[tokio::test]
2487 async fn test_merkle_paid_node_address_mismatch_rejected() {
2488 let verifier = create_test_verifier();
2489 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
2490
2491 {
2494 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2495 depth: 2,
2496 merkle_payment_timestamp: ts,
2497 paid_node_addresses: vec![
2498 (RewardsAddress::new([0u8; 20]), 0, Amount::from(2048u64)),
2501 (RewardsAddress::new([0xFF; 20]), 1, Amount::from(2048u64)),
2503 ],
2504 };
2505 verifier.pool_cache.lock().put(pool_hash, info);
2506 }
2507
2508 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
2509
2510 assert!(result.is_err(), "Should reject paid node address mismatch");
2511 let err_msg = format!("{}", result.expect_err("should fail"));
2512 assert!(
2513 err_msg.contains("address mismatch"),
2514 "Error should mention address mismatch: {err_msg}"
2515 );
2516 }
2517
2518 #[tokio::test]
2519 async fn test_merkle_wrong_depth_rejected() {
2520 let verifier = create_test_verifier();
2521 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
2522
2523 {
2526 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2527 depth: 3,
2528 merkle_payment_timestamp: ts,
2529 paid_node_addresses: vec![(
2530 RewardsAddress::new([0u8; 20]),
2531 0,
2532 Amount::from(1024u64),
2533 )],
2534 };
2535 verifier.pool_cache.lock().put(pool_hash, info);
2536 }
2537
2538 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
2539
2540 assert!(
2541 result.is_err(),
2542 "Should reject mismatched depth vs paid node count"
2543 );
2544 let err_msg = format!("{}", result.expect_err("should fail"));
2545 assert!(
2546 err_msg.contains("Wrong number of paid nodes")
2547 || err_msg.contains("verification failed"),
2548 "Error should mention depth/count mismatch: {err_msg}"
2549 );
2550 }
2551
2552 #[tokio::test]
2553 async fn test_merkle_underpayment_rejected() {
2554 let verifier = create_test_verifier();
2555 let (xorname, tagged_proof, pool_hash, ts) = make_valid_merkle_proof_bytes();
2556
2557 {
2561 let info = evmlib::merkle_payments::OnChainPaymentInfo {
2562 depth: 2,
2563 merkle_payment_timestamp: ts,
2564 paid_node_addresses: vec![
2565 (RewardsAddress::new([0u8; 20]), 0, Amount::from(1u64)),
2566 (RewardsAddress::new([1u8; 20]), 1, Amount::from(1u64)),
2567 ],
2568 };
2569 verifier.pool_cache.lock().put(pool_hash, info);
2570 }
2571
2572 let result = verifier.verify_payment(&xorname, Some(&tagged_proof)).await;
2573
2574 assert!(
2575 result.is_err(),
2576 "Should reject merkle payment where paid amount < expected per-node amount"
2577 );
2578 let err_msg = format!("{}", result.expect_err("should fail"));
2579 assert!(
2580 err_msg.contains("Underpayment"),
2581 "Error should mention underpayment: {err_msg}"
2582 );
2583 }
2584}