Skip to main content

ant_node/payment/
verifier.rs

1//! Payment verifier with LRU cache and EVM verification.
2//!
3//! This is the core payment verification logic for ant-node.
4//! All new data requires EVM payment on Arbitrum (no free tier).
5
6use crate::error::{Error, Result};
7use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
8use crate::payment::proof::{
9    deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
10};
11use crate::payment::quote::{verify_quote_content, verify_quote_signature};
12use crate::payment::single_node::REQUIRED_QUOTES;
13use ant_evm::merkle_payments::OnChainPaymentInfo;
14use ant_evm::{ProofOfPayment, RewardsAddress};
15use evmlib::contract::merkle_payment_vault;
16use evmlib::merkle_batch_payment::PoolHash;
17use evmlib::Network as EvmNetwork;
18use lru::LruCache;
19use parking_lot::Mutex;
20use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
21use std::num::NonZeroUsize;
22use std::time::SystemTime;
23use tracing::{debug, info};
24
25/// Minimum allowed size for a payment proof in bytes.
26///
27/// This minimum ensures the proof contains at least a basic cryptographic hash or identifier.
28/// Proofs smaller than this are rejected as they cannot contain sufficient payment information.
29const MIN_PAYMENT_PROOF_SIZE_BYTES: usize = 32;
30
31/// Maximum allowed size for a payment proof in bytes (256 KB).
32///
33/// Single-node proofs with 5 ML-DSA-65 quotes reach ~30 KB.
34/// Merkle proofs include 16 candidate nodes (each with ~1,952-byte ML-DSA pub key
35/// and ~3,309-byte signature) plus merkle branch hashes, totaling ~130 KB.
36/// 256 KB provides headroom while still capping memory during verification.
37const MAX_PAYMENT_PROOF_SIZE_BYTES: usize = 262_144;
38
39/// Maximum age of a payment quote before it's considered expired (24 hours).
40/// Prevents replaying old cheap quotes against nearly-full nodes.
41const QUOTE_MAX_AGE_SECS: u64 = 86_400;
42
43/// Maximum allowed clock skew for quote timestamps (60 seconds).
44/// Accounts for NTP synchronization differences between P2P nodes.
45const QUOTE_CLOCK_SKEW_TOLERANCE_SECS: u64 = 60;
46
47/// Configuration for EVM payment verification.
48///
49/// EVM verification is always on. All new data requires on-chain
50/// payment verification. The network field selects which EVM chain to use.
51#[derive(Debug, Clone)]
52pub struct EvmVerifierConfig {
53    /// EVM network to use (Arbitrum One, Arbitrum Sepolia, etc.)
54    pub network: EvmNetwork,
55}
56
57impl Default for EvmVerifierConfig {
58    fn default() -> Self {
59        Self {
60            network: EvmNetwork::ArbitrumOne,
61        }
62    }
63}
64
65/// Configuration for the payment verifier.
66///
67/// All new data requires EVM payment on Arbitrum. The cache stores
68/// previously verified payments to avoid redundant on-chain lookups.
69#[derive(Debug, Clone)]
70pub struct PaymentVerifierConfig {
71    /// EVM verifier configuration.
72    pub evm: EvmVerifierConfig,
73    /// Cache capacity (number of `XorName` values to cache).
74    pub cache_capacity: usize,
75    /// Local node's rewards address.
76    /// The verifier rejects payments that don't include this node as a recipient.
77    pub local_rewards_address: RewardsAddress,
78}
79
80/// Status returned by payment verification.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PaymentStatus {
83    /// Data was found in local cache - previously paid.
84    CachedAsVerified,
85    /// New data - payment required.
86    PaymentRequired,
87    /// Payment was provided and verified.
88    PaymentVerified,
89}
90
91impl PaymentStatus {
92    /// Returns true if the data can be stored (cached or payment verified).
93    #[must_use]
94    pub fn can_store(&self) -> bool {
95        matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
96    }
97
98    /// Returns true if this status indicates the data was already paid for.
99    #[must_use]
100    pub fn is_cached(&self) -> bool {
101        matches!(self, Self::CachedAsVerified)
102    }
103}
104
105/// Default capacity for the merkle pool cache (number of pool hashes to cache).
106const DEFAULT_POOL_CACHE_CAPACITY: usize = 1_000;
107
108/// Main payment verifier for ant-node.
109///
110/// Uses:
111/// 1. LRU cache for fast lookups of previously verified `XorName` values
112/// 2. EVM payment verification for new data (always required)
113/// 3. Pool-level cache for merkle batch payments (avoids repeated on-chain queries)
114pub struct PaymentVerifier {
115    /// LRU cache of verified `XorName` values.
116    cache: VerifiedCache,
117    /// LRU cache of verified merkle pool hashes → on-chain payment info.
118    pool_cache: Mutex<LruCache<PoolHash, OnChainPaymentInfo>>,
119    /// Configuration.
120    config: PaymentVerifierConfig,
121}
122
123impl PaymentVerifier {
124    /// Create a new payment verifier.
125    #[must_use]
126    pub fn new(config: PaymentVerifierConfig) -> Self {
127        const _: () = assert!(
128            DEFAULT_POOL_CACHE_CAPACITY > 0,
129            "pool cache capacity must be > 0"
130        );
131        let cache = VerifiedCache::with_capacity(config.cache_capacity);
132        let pool_cache_size =
133            NonZeroUsize::new(DEFAULT_POOL_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN);
134        let pool_cache = Mutex::new(LruCache::new(pool_cache_size));
135
136        let cache_capacity = config.cache_capacity;
137        info!("Payment verifier initialized (cache_capacity={cache_capacity}, evm=always-on, pool_cache={DEFAULT_POOL_CACHE_CAPACITY})");
138
139        Self {
140            cache,
141            pool_cache,
142            config,
143        }
144    }
145
146    /// Check if payment is required for the given `XorName`.
147    ///
148    /// This is the main entry point for payment verification:
149    /// 1. Check LRU cache (fast path)
150    /// 2. If not cached, payment is required
151    ///
152    /// # Arguments
153    ///
154    /// * `xorname` - The content-addressed name of the data
155    ///
156    /// # Returns
157    ///
158    /// * `PaymentStatus::CachedAsVerified` - Found in local cache (previously paid)
159    /// * `PaymentStatus::PaymentRequired` - Not cached (payment required)
160    pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
161        // Check LRU cache (fast path)
162        if self.cache.contains(xorname) {
163            if tracing::enabled!(tracing::Level::DEBUG) {
164                debug!("Data {} found in verified cache", hex::encode(xorname));
165            }
166            return PaymentStatus::CachedAsVerified;
167        }
168
169        // Not in cache - payment required
170        if tracing::enabled!(tracing::Level::DEBUG) {
171            debug!(
172                "Data {} not in cache - payment required",
173                hex::encode(xorname)
174            );
175        }
176        PaymentStatus::PaymentRequired
177    }
178
179    /// Verify that a PUT request has valid payment.
180    ///
181    /// This is the complete payment verification flow:
182    /// 1. Check if data is in cache (previously paid)
183    /// 2. If not, verify the provided payment proof
184    ///
185    /// # Arguments
186    ///
187    /// * `xorname` - The content-addressed name of the data
188    /// * `payment_proof` - Optional payment proof (required if not in cache)
189    ///
190    /// # Returns
191    ///
192    /// * `Ok(PaymentStatus)` - Verification succeeded
193    /// * `Err(Error::Payment)` - No payment and not cached, or payment invalid
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if payment is required but not provided, or if payment is invalid.
198    pub async fn verify_payment(
199        &self,
200        xorname: &XorName,
201        payment_proof: Option<&[u8]>,
202    ) -> Result<PaymentStatus> {
203        // First check if payment is required
204        let status = self.check_payment_required(xorname);
205
206        match status {
207            PaymentStatus::CachedAsVerified => {
208                // No payment needed - already in cache
209                Ok(status)
210            }
211            PaymentStatus::PaymentRequired => {
212                // EVM verification is always on — verify the proof
213                if let Some(proof) = payment_proof {
214                    let proof_len = proof.len();
215                    if proof_len < MIN_PAYMENT_PROOF_SIZE_BYTES {
216                        return Err(Error::Payment(format!(
217                            "Payment proof too small: {proof_len} bytes (min {MIN_PAYMENT_PROOF_SIZE_BYTES})"
218                        )));
219                    }
220                    if proof_len > MAX_PAYMENT_PROOF_SIZE_BYTES {
221                        return Err(Error::Payment(format!(
222                            "Payment proof too large: {proof_len} bytes (max {MAX_PAYMENT_PROOF_SIZE_BYTES} bytes)"
223                        )));
224                    }
225
226                    // Detect proof type from version tag byte
227                    match detect_proof_type(proof) {
228                        Some(ProofType::Merkle) => {
229                            self.verify_merkle_payment(xorname, proof).await?;
230                        }
231                        Some(ProofType::SingleNode) => {
232                            let (payment, tx_hashes) = deserialize_proof(proof).map_err(|e| {
233                                Error::Payment(format!("Failed to deserialize payment proof: {e}"))
234                            })?;
235
236                            if !tx_hashes.is_empty() {
237                                debug!("Proof includes {} transaction hash(es)", tx_hashes.len());
238                            }
239
240                            self.verify_evm_payment(xorname, &payment).await?;
241                        }
242                        None => {
243                            let tag = proof.first().copied().unwrap_or(0);
244                            return Err(Error::Payment(format!(
245                                "Unknown payment proof type tag: 0x{tag:02x}"
246                            )));
247                        }
248                    }
249
250                    // Cache the verified xorname
251                    self.cache.insert(*xorname);
252
253                    Ok(PaymentStatus::PaymentVerified)
254                } else {
255                    // No payment provided in production mode
256                    Err(Error::Payment(format!(
257                        "Payment required for new data {}",
258                        hex::encode(xorname)
259                    )))
260                }
261            }
262            PaymentStatus::PaymentVerified => Err(Error::Payment(
263                "Unexpected PaymentVerified status from check_payment_required".to_string(),
264            )),
265        }
266    }
267
268    /// Get cache statistics.
269    #[must_use]
270    pub fn cache_stats(&self) -> CacheStats {
271        self.cache.stats()
272    }
273
274    /// Get the number of cached entries.
275    #[must_use]
276    pub fn cache_len(&self) -> usize {
277        self.cache.len()
278    }
279
280    /// Pre-populate the payment cache for a given address.
281    ///
282    /// This marks the address as already paid, so subsequent `verify_payment`
283    /// calls will return `CachedAsVerified` without on-chain verification.
284    /// Useful for test setups where real EVM payment is not needed.
285    #[cfg(any(test, feature = "test-utils"))]
286    pub fn cache_insert(&self, xorname: XorName) {
287        self.cache.insert(xorname);
288    }
289
290    /// Verify an EVM payment proof.
291    ///
292    /// This verification ALWAYS validates payment proofs on-chain.
293    /// It verifies that:
294    /// 1. All quotes target the correct content address (xorname binding)
295    /// 2. All quote ML-DSA-65 signatures are valid (offloaded to a blocking
296    ///    thread via `spawn_blocking` since post-quantum signature verification
297    ///    is CPU-intensive)
298    /// 3. The payment was made on-chain via the EVM payment vault contract
299    ///
300    /// For unit tests that don't need on-chain verification, pre-populate
301    /// the cache so `verify_payment` returns `CachedAsVerified` before
302    /// reaching this method.
303    async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
304        if tracing::enabled!(tracing::Level::DEBUG) {
305            let xorname_hex = hex::encode(xorname);
306            let quote_count = payment.peer_quotes.len();
307            debug!("Verifying EVM payment for {xorname_hex} with {quote_count} quotes");
308        }
309
310        Self::validate_quote_structure(payment)?;
311        Self::validate_quote_content(payment, xorname)?;
312        Self::validate_quote_timestamps(payment)?;
313        Self::validate_peer_bindings(payment)?;
314        self.validate_local_recipient(payment)?;
315
316        // Verify quote signatures (CPU-bound, run off async runtime)
317        let peer_quotes = payment.peer_quotes.clone();
318        tokio::task::spawn_blocking(move || {
319            for (encoded_peer_id, quote) in &peer_quotes {
320                if !verify_quote_signature(quote) {
321                    return Err(Error::Payment(
322                        format!("Quote ML-DSA-65 signature verification failed for peer {encoded_peer_id:?}"),
323                    ));
324                }
325            }
326            Ok(())
327        })
328        .await
329        .map_err(|e| Error::Payment(format!("Signature verification task failed: {e}")))??;
330
331        // Verify on-chain payment.
332        //
333        // The SingleNode payment model pays only the median-priced quote (at 3x)
334        // and sends Amount::ZERO for the other 4. evmlib's pay_for_quotes()
335        // filters out zero-amount payments, so only 1 quote has an on-chain
336        // record. The contract's verifyPayment() returns amountPaid=0 and
337        // isValid=false for unpaid quotes, which is expected.
338        //
339        // We use the amountPaid field to distinguish paid from unpaid results:
340        // - At least one quote must have been paid (amountPaid > 0)
341        // - ALL paid quotes must be valid (isValid=true)
342        // - Unpaid quotes (amountPaid=0) are allowed to be invalid
343        //
344        // This matches autonomi's strict verification model (all paid must be
345        // valid) while accommodating payment models that don't pay every quote.
346        let payment_digest = payment.digest();
347        if payment_digest.is_empty() {
348            return Err(Error::Payment("Payment has no quotes".to_string()));
349        }
350
351        let payment_verifications: Vec<_> = payment_digest
352            .into_iter()
353            .map(
354                evmlib::contract::payment_vault::interface::IPaymentVault::PaymentVerification::from,
355            )
356            .collect();
357
358        let provider = evmlib::utils::http_provider(self.config.evm.network.rpc_url().clone());
359        let handler = evmlib::contract::payment_vault::handler::PaymentVaultHandler::new(
360            *self.config.evm.network.data_payments_address(),
361            provider,
362        );
363
364        let results = handler
365            .verify_payment(payment_verifications)
366            .await
367            .map_err(|e| {
368                Error::Payment(format!(
369                    "EVM verification error for {}: {e}",
370                    hex::encode(xorname)
371                ))
372            })?;
373
374        let paid_results: Vec<_> = results
375            .iter()
376            .filter(|r| r.amountPaid > evmlib::common::U256::ZERO)
377            .collect();
378
379        if paid_results.is_empty() {
380            return Err(Error::Payment(format!(
381                "Payment verification failed on-chain for {} (no paid quotes found)",
382                hex::encode(xorname)
383            )));
384        }
385
386        for result in &paid_results {
387            if !result.isValid {
388                return Err(Error::Payment(format!(
389                    "Payment verification failed on-chain for {} (paid quote is invalid)",
390                    hex::encode(xorname)
391                )));
392            }
393        }
394
395        if tracing::enabled!(tracing::Level::INFO) {
396            let valid_count = paid_results.len();
397            info!(
398                "EVM payment verified for {} ({valid_count} paid and valid, {} total results)",
399                hex::encode(xorname),
400                results.len()
401            );
402        }
403        Ok(())
404    }
405
406    /// Validate quote count, uniqueness, and basic structure.
407    fn validate_quote_structure(payment: &ProofOfPayment) -> Result<()> {
408        if payment.peer_quotes.is_empty() {
409            return Err(Error::Payment("Payment has no quotes".to_string()));
410        }
411
412        let quote_count = payment.peer_quotes.len();
413        if quote_count != REQUIRED_QUOTES {
414            return Err(Error::Payment(format!(
415                "Payment must have exactly {REQUIRED_QUOTES} quotes, got {quote_count}"
416            )));
417        }
418
419        let mut seen: Vec<&ant_evm::EncodedPeerId> = Vec::with_capacity(quote_count);
420        for (encoded_peer_id, _) in &payment.peer_quotes {
421            if seen.contains(&encoded_peer_id) {
422                return Err(Error::Payment(format!(
423                    "Duplicate peer ID in payment quotes: {encoded_peer_id:?}"
424                )));
425            }
426            seen.push(encoded_peer_id);
427        }
428
429        Ok(())
430    }
431
432    /// Verify all quotes target the correct content address.
433    fn validate_quote_content(payment: &ProofOfPayment, xorname: &XorName) -> Result<()> {
434        for (encoded_peer_id, quote) in &payment.peer_quotes {
435            if !verify_quote_content(quote, xorname) {
436                return Err(Error::Payment(format!(
437                    "Quote content address mismatch for peer {encoded_peer_id:?}: expected {}, got {}",
438                    hex::encode(xorname),
439                    hex::encode(quote.content.0)
440                )));
441            }
442        }
443        Ok(())
444    }
445
446    /// Verify quote freshness — reject stale or excessively future quotes.
447    fn validate_quote_timestamps(payment: &ProofOfPayment) -> Result<()> {
448        let now = SystemTime::now();
449        for (encoded_peer_id, quote) in &payment.peer_quotes {
450            match now.duration_since(quote.timestamp) {
451                Ok(age) => {
452                    if age.as_secs() > QUOTE_MAX_AGE_SECS {
453                        return Err(Error::Payment(format!(
454                            "Quote from peer {encoded_peer_id:?} expired: age {}s exceeds max {QUOTE_MAX_AGE_SECS}s",
455                            age.as_secs()
456                        )));
457                    }
458                }
459                Err(_) => {
460                    if let Ok(skew) = quote.timestamp.duration_since(now) {
461                        if skew.as_secs() > QUOTE_CLOCK_SKEW_TOLERANCE_SECS {
462                            return Err(Error::Payment(format!(
463                                "Quote from peer {encoded_peer_id:?} has timestamp {}s in the future \
464                                 (exceeds {QUOTE_CLOCK_SKEW_TOLERANCE_SECS}s tolerance)",
465                                skew.as_secs()
466                            )));
467                        }
468                    } else {
469                        return Err(Error::Payment(format!(
470                            "Quote from peer {encoded_peer_id:?} has invalid timestamp"
471                        )));
472                    }
473                }
474            }
475        }
476        Ok(())
477    }
478
479    /// Verify each quote's `pub_key` matches the claimed peer ID via BLAKE3.
480    fn validate_peer_bindings(payment: &ProofOfPayment) -> Result<()> {
481        for (encoded_peer_id, quote) in &payment.peer_quotes {
482            let expected_peer_id = peer_id_from_public_key_bytes(&quote.pub_key)
483                .map_err(|e| Error::Payment(format!("Invalid ML-DSA public key in quote: {e}")))?;
484
485            let libp2p_peer_id = encoded_peer_id
486                .to_peer_id()
487                .map_err(|e| Error::Payment(format!("Invalid encoded peer ID: {e}")))?;
488            let peer_id_bytes = libp2p_peer_id.to_bytes();
489            let raw_peer_bytes = if peer_id_bytes.len() > 2 {
490                &peer_id_bytes[2..]
491            } else {
492                return Err(Error::Payment(format!(
493                    "Invalid encoded peer ID: too short ({} bytes)",
494                    peer_id_bytes.len()
495                )));
496            };
497
498            if expected_peer_id.as_bytes() != raw_peer_bytes {
499                return Err(Error::Payment(format!(
500                    "Quote pub_key does not belong to claimed peer {encoded_peer_id:?}: \
501                     BLAKE3(pub_key) = {}, peer_id = {}",
502                    expected_peer_id.to_hex(),
503                    hex::encode(raw_peer_bytes)
504                )));
505            }
506        }
507        Ok(())
508    }
509
510    /// Verify a merkle batch payment proof.
511    ///
512    /// This verification flow:
513    /// 1. Deserialize the `MerklePaymentProof`
514    /// 2. Check pool cache for previously verified pool hash
515    /// 3. If not cached, query on-chain for payment info
516    /// 4. Validate the proof against on-chain data
517    /// 5. Cache the pool hash for subsequent chunk verifications in the same batch
518    #[allow(clippy::too_many_lines)]
519    async fn verify_merkle_payment(&self, xorname: &XorName, proof_bytes: &[u8]) -> Result<()> {
520        if tracing::enabled!(tracing::Level::DEBUG) {
521            debug!("Verifying merkle payment for {}", hex::encode(xorname));
522        }
523
524        // Deserialize the merkle proof
525        let merkle_proof = deserialize_merkle_proof(proof_bytes)
526            .map_err(|e| Error::Payment(format!("Failed to deserialize merkle proof: {e}")))?;
527
528        // Verify the address in the proof matches the xorname being stored
529        if merkle_proof.address.0 != *xorname {
530            return Err(Error::Payment(format!(
531                "Merkle proof address mismatch: proof is for {}, but storing {}",
532                hex::encode(merkle_proof.address.0),
533                hex::encode(xorname)
534            )));
535        }
536
537        let pool_hash = merkle_proof.winner_pool_hash();
538
539        // Check pool cache first
540        let cached_info = {
541            let mut pool_cache = self.pool_cache.lock();
542            pool_cache.get(&pool_hash).cloned()
543        };
544
545        let payment_info = if let Some(info) = cached_info {
546            debug!("Pool cache hit for hash {}", hex::encode(pool_hash));
547            info
548        } else {
549            // Query on-chain for payment info
550            let info =
551                merkle_payment_vault::get_merkle_payment_info(&self.config.evm.network, pool_hash)
552                    .await
553                    .map_err(|e| {
554                        Error::Payment(format!(
555                            "Failed to query merkle payment info for pool {}: {e}",
556                            hex::encode(pool_hash)
557                        ))
558                    })?;
559
560            let paid_node_addresses: Vec<_> = info
561                .paidNodeAddresses
562                .iter()
563                .map(|pna| (pna.rewardsAddress, usize::from(pna.poolIndex)))
564                .collect();
565
566            let on_chain_info = OnChainPaymentInfo {
567                depth: info.depth,
568                merkle_payment_timestamp: info.merklePaymentTimestamp,
569                paid_node_addresses,
570            };
571
572            // Cache the pool info for subsequent chunks in the same batch
573            {
574                let mut pool_cache = self.pool_cache.lock();
575                pool_cache.put(pool_hash, on_chain_info.clone());
576            }
577
578            debug!(
579                "Queried on-chain merkle payment info for pool {}: depth={}, timestamp={}, paid_nodes={}",
580                hex::encode(pool_hash),
581                on_chain_info.depth,
582                on_chain_info.merkle_payment_timestamp,
583                on_chain_info.paid_node_addresses.len()
584            );
585
586            on_chain_info
587        };
588
589        // pool_hash was derived from merkle_proof.winner_pool and used to query
590        // the contract. The contract only returns data if a payment exists for that
591        // hash. The ML-DSA signature check below ensures the pool contents are
592        // authentic (nodes actually signed their candidate quotes).
593
594        // Verify ML-DSA-65 signatures and timestamp/data_type consistency
595        // on all candidate nodes in the winner pool.
596        for candidate in &merkle_proof.winner_pool.candidate_nodes {
597            if !crate::payment::verify_merkle_candidate_signature(candidate) {
598                return Err(Error::Payment(format!(
599                    "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})",
600                    candidate.reward_address
601                )));
602            }
603            if candidate.merkle_payment_timestamp != payment_info.merkle_payment_timestamp {
604                return Err(Error::Payment(format!(
605                    "Candidate timestamp mismatch: expected {}, got {} (reward: {})",
606                    payment_info.merkle_payment_timestamp,
607                    candidate.merkle_payment_timestamp,
608                    candidate.reward_address
609                )));
610            }
611        }
612
613        // Get the root from the winner pool's midpoint proof
614        let smart_contract_root = merkle_proof.winner_pool.midpoint_proof.root();
615
616        // Verify the cryptographic merkle proofs (address belongs to tree,
617        // midpoint belongs to tree, roots match, timestamps valid).
618        ant_evm::merkle_payments::verify_merkle_proof(
619            &merkle_proof.address,
620            &merkle_proof.data_proof,
621            &merkle_proof.winner_pool.midpoint_proof,
622            payment_info.depth,
623            smart_contract_root,
624            payment_info.merkle_payment_timestamp,
625        )
626        .map_err(|e| {
627            Error::Payment(format!(
628                "Merkle proof verification failed for {}: {e}",
629                hex::encode(xorname)
630            ))
631        })?;
632
633        // Verify paid node count matches depth
634        if payment_info.paid_node_addresses.len() != payment_info.depth as usize {
635            return Err(Error::Payment(format!(
636                "Wrong number of paid nodes: expected {}, got {}",
637                payment_info.depth,
638                payment_info.paid_node_addresses.len()
639            )));
640        }
641
642        // Verify paid node indices are valid within the candidate pool.
643        //
644        // Note: unlike single-node payments, merkle proofs are NOT bound to a
645        // specific storing node. The contract pays `depth` random nodes from the
646        // winner pool; the storing node is whichever close-group peer the client
647        // routes the chunk to. There is no local-recipient check here because
648        // any node that can verify the merkle proof is allowed to store the chunk.
649        // Replay protection comes from the per-address proof binding (each proof
650        // is for a specific XorName in the paid tree).
651        for (addr, idx) in &payment_info.paid_node_addresses {
652            let node = merkle_proof
653                .winner_pool
654                .candidate_nodes
655                .get(*idx)
656                .ok_or_else(|| {
657                    Error::Payment(format!(
658                        "Paid node index {idx} out of bounds for pool size {}",
659                        merkle_proof.winner_pool.candidate_nodes.len()
660                    ))
661                })?;
662            if node.reward_address != *addr {
663                return Err(Error::Payment(format!(
664                    "Paid node address mismatch at index {idx}: expected {addr}, got {}",
665                    node.reward_address
666                )));
667            }
668        }
669
670        if tracing::enabled!(tracing::Level::INFO) {
671            info!(
672                "Merkle payment verified for {} (pool: {})",
673                hex::encode(xorname),
674                hex::encode(pool_hash)
675            );
676        }
677
678        Ok(())
679    }
680
681    /// Verify this node is among the paid recipients.
682    fn validate_local_recipient(&self, payment: &ProofOfPayment) -> Result<()> {
683        let local_addr = &self.config.local_rewards_address;
684        let is_recipient = payment
685            .peer_quotes
686            .iter()
687            .any(|(_, quote)| quote.rewards_address == *local_addr);
688        if !is_recipient {
689            return Err(Error::Payment(
690                "Payment proof does not include this node as a recipient".to_string(),
691            ));
692        }
693        Ok(())
694    }
695}
696
697#[cfg(test)]
698#[allow(clippy::expect_used)]
699mod tests {
700    use super::*;
701
702    /// Create a verifier for unit tests. EVM is always on, but tests can
703    /// pre-populate the cache to bypass on-chain verification.
704    fn create_test_verifier() -> PaymentVerifier {
705        let config = PaymentVerifierConfig {
706            evm: EvmVerifierConfig::default(),
707            cache_capacity: 100,
708            local_rewards_address: RewardsAddress::new([1u8; 20]),
709        };
710        PaymentVerifier::new(config)
711    }
712
713    #[test]
714    fn test_payment_required_for_new_data() {
715        let verifier = create_test_verifier();
716        let xorname = [1u8; 32];
717
718        // All uncached data requires payment
719        let status = verifier.check_payment_required(&xorname);
720        assert_eq!(status, PaymentStatus::PaymentRequired);
721    }
722
723    #[test]
724    fn test_cache_hit() {
725        let verifier = create_test_verifier();
726        let xorname = [1u8; 32];
727
728        // Manually add to cache
729        verifier.cache.insert(xorname);
730
731        // Should return CachedAsVerified
732        let status = verifier.check_payment_required(&xorname);
733        assert_eq!(status, PaymentStatus::CachedAsVerified);
734    }
735
736    #[tokio::test]
737    async fn test_verify_payment_without_proof_rejected() {
738        let verifier = create_test_verifier();
739        let xorname = [1u8; 32];
740
741        // No proof provided => should return an error (EVM is always on)
742        let result = verifier.verify_payment(&xorname, None).await;
743        assert!(
744            result.is_err(),
745            "Expected Err without proof, got: {result:?}"
746        );
747    }
748
749    #[tokio::test]
750    async fn test_verify_payment_cached() {
751        let verifier = create_test_verifier();
752        let xorname = [1u8; 32];
753
754        // Add to cache — simulates previously-paid data
755        verifier.cache.insert(xorname);
756
757        // Should succeed without payment (cached)
758        let result = verifier.verify_payment(&xorname, None).await;
759        assert!(result.is_ok());
760        assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
761    }
762
763    #[test]
764    fn test_payment_status_can_store() {
765        assert!(PaymentStatus::CachedAsVerified.can_store());
766        assert!(PaymentStatus::PaymentVerified.can_store());
767        assert!(!PaymentStatus::PaymentRequired.can_store());
768    }
769
770    #[test]
771    fn test_payment_status_is_cached() {
772        assert!(PaymentStatus::CachedAsVerified.is_cached());
773        assert!(!PaymentStatus::PaymentVerified.is_cached());
774        assert!(!PaymentStatus::PaymentRequired.is_cached());
775    }
776
777    #[tokio::test]
778    async fn test_cache_preload_bypasses_evm() {
779        let verifier = create_test_verifier();
780        let xorname = [42u8; 32];
781
782        // Not yet cached — should require payment
783        assert_eq!(
784            verifier.check_payment_required(&xorname),
785            PaymentStatus::PaymentRequired
786        );
787
788        // Pre-populate cache (simulates a previous successful payment)
789        verifier.cache.insert(xorname);
790
791        // Now the xorname should be cached
792        assert_eq!(
793            verifier.check_payment_required(&xorname),
794            PaymentStatus::CachedAsVerified
795        );
796    }
797
798    #[tokio::test]
799    async fn test_proof_too_small() {
800        let verifier = create_test_verifier();
801        let xorname = [1u8; 32];
802
803        // Proof smaller than MIN_PAYMENT_PROOF_SIZE_BYTES
804        let small_proof = vec![0u8; MIN_PAYMENT_PROOF_SIZE_BYTES - 1];
805        let result = verifier.verify_payment(&xorname, Some(&small_proof)).await;
806        assert!(result.is_err());
807        let err_msg = format!("{}", result.expect_err("should fail"));
808        assert!(
809            err_msg.contains("too small"),
810            "Error should mention 'too small': {err_msg}"
811        );
812    }
813
814    #[tokio::test]
815    async fn test_proof_too_large() {
816        let verifier = create_test_verifier();
817        let xorname = [2u8; 32];
818
819        // Proof larger than MAX_PAYMENT_PROOF_SIZE_BYTES
820        let large_proof = vec![0u8; MAX_PAYMENT_PROOF_SIZE_BYTES + 1];
821        let result = verifier.verify_payment(&xorname, Some(&large_proof)).await;
822        assert!(result.is_err());
823        let err_msg = format!("{}", result.expect_err("should fail"));
824        assert!(
825            err_msg.contains("too large"),
826            "Error should mention 'too large': {err_msg}"
827        );
828    }
829
830    #[tokio::test]
831    async fn test_proof_at_min_boundary_unknown_tag() {
832        let verifier = create_test_verifier();
833        let xorname = [3u8; 32];
834
835        // Exactly MIN_PAYMENT_PROOF_SIZE_BYTES with unknown tag — rejected
836        let boundary_proof = vec![0xFFu8; MIN_PAYMENT_PROOF_SIZE_BYTES];
837        let result = verifier
838            .verify_payment(&xorname, Some(&boundary_proof))
839            .await;
840        assert!(result.is_err());
841        let err_msg = format!("{}", result.expect_err("should fail"));
842        assert!(
843            err_msg.contains("Unknown payment proof type tag"),
844            "Error should mention unknown tag: {err_msg}"
845        );
846    }
847
848    #[tokio::test]
849    async fn test_proof_at_max_boundary_unknown_tag() {
850        let verifier = create_test_verifier();
851        let xorname = [4u8; 32];
852
853        // Exactly MAX_PAYMENT_PROOF_SIZE_BYTES with unknown tag — rejected
854        let boundary_proof = vec![0xFFu8; MAX_PAYMENT_PROOF_SIZE_BYTES];
855        let result = verifier
856            .verify_payment(&xorname, Some(&boundary_proof))
857            .await;
858        assert!(result.is_err());
859        let err_msg = format!("{}", result.expect_err("should fail"));
860        assert!(
861            err_msg.contains("Unknown payment proof type tag"),
862            "Error should mention unknown tag: {err_msg}"
863        );
864    }
865
866    #[tokio::test]
867    async fn test_malformed_single_node_proof() {
868        let verifier = create_test_verifier();
869        let xorname = [5u8; 32];
870
871        // Valid tag (0x01) but garbage payload — should fail deserialization
872        let mut garbage = vec![crate::ant_protocol::PROOF_TAG_SINGLE_NODE];
873        garbage.extend_from_slice(&[0xAB; 63]);
874        let result = verifier.verify_payment(&xorname, Some(&garbage)).await;
875        assert!(result.is_err());
876        let err_msg = format!("{}", result.expect_err("should fail"));
877        assert!(
878            err_msg.contains("deserialize") || err_msg.contains("Failed"),
879            "Error should mention deserialization failure: {err_msg}"
880        );
881    }
882
883    #[test]
884    fn test_cache_len_getter() {
885        let verifier = create_test_verifier();
886        assert_eq!(verifier.cache_len(), 0);
887
888        verifier.cache.insert([10u8; 32]);
889        assert_eq!(verifier.cache_len(), 1);
890
891        verifier.cache.insert([20u8; 32]);
892        assert_eq!(verifier.cache_len(), 2);
893    }
894
895    #[test]
896    fn test_cache_stats_after_operations() {
897        let verifier = create_test_verifier();
898        let xorname = [7u8; 32];
899
900        // Miss
901        verifier.check_payment_required(&xorname);
902        let stats = verifier.cache_stats();
903        assert_eq!(stats.misses, 1);
904        assert_eq!(stats.hits, 0);
905
906        // Insert and hit
907        verifier.cache.insert(xorname);
908        verifier.check_payment_required(&xorname);
909        let stats = verifier.cache_stats();
910        assert_eq!(stats.hits, 1);
911        assert_eq!(stats.misses, 1);
912        assert_eq!(stats.additions, 1);
913    }
914
915    #[tokio::test]
916    async fn test_concurrent_cache_lookups() {
917        let verifier = std::sync::Arc::new(create_test_verifier());
918
919        // Pre-populate cache for all 10 xornames
920        for i in 0..10u8 {
921            verifier.cache.insert([i; 32]);
922        }
923
924        let mut handles = Vec::new();
925        for i in 0..10u8 {
926            let v = verifier.clone();
927            handles.push(tokio::spawn(async move {
928                let xorname = [i; 32];
929                v.verify_payment(&xorname, None).await
930            }));
931        }
932
933        for handle in handles {
934            let result = handle.await.expect("task panicked");
935            assert!(result.is_ok());
936            assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
937        }
938
939        assert_eq!(verifier.cache_len(), 10);
940    }
941
942    #[test]
943    fn test_default_evm_config() {
944        let _config = EvmVerifierConfig::default();
945        // EVM is always on — default network is ArbitrumOne
946    }
947
948    #[test]
949    fn test_real_ml_dsa_proof_size_within_limits() {
950        use crate::payment::metrics::QuotingMetricsTracker;
951        use crate::payment::proof::PaymentProof;
952        use crate::payment::quote::{QuoteGenerator, XorName};
953        use alloy::primitives::FixedBytes;
954        use ant_evm::{EncodedPeerId, RewardsAddress};
955        use saorsa_core::MlDsa65;
956        use saorsa_pqc::pqc::types::MlDsaSecretKey;
957        use saorsa_pqc::pqc::MlDsaOperations;
958
959        let ml_dsa = MlDsa65::new();
960        let mut peer_quotes = Vec::new();
961
962        for i in 0..5u8 {
963            let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
964
965            let rewards_address = RewardsAddress::new([i; 20]);
966            let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
967            let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
968
969            let pub_key_bytes = public_key.as_bytes().to_vec();
970            let sk_bytes = secret_key.as_bytes().to_vec();
971            generator.set_signer(pub_key_bytes, move |msg| {
972                let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
973                let ml_dsa = MlDsa65::new();
974                ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
975            });
976
977            let content: XorName = [i; 32];
978            let quote = generator.create_quote(content, 4096, 0).expect("quote");
979
980            let keypair = libp2p::identity::Keypair::generate_ed25519();
981            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
982            peer_quotes.push((EncodedPeerId::from(peer_id), quote));
983        }
984
985        let proof = PaymentProof {
986            proof_of_payment: ProofOfPayment { peer_quotes },
987            tx_hashes: vec![FixedBytes::from([0xABu8; 32])],
988        };
989
990        let proof_bytes =
991            crate::payment::proof::serialize_single_node_proof(&proof).expect("serialize");
992
993        // 5 ML-DSA-65 quotes with ~1952-byte pub keys and ~3309-byte signatures
994        // should produce a proof in the 20-60 KB range
995        assert!(
996            proof_bytes.len() > 20_000,
997            "Real 5-quote ML-DSA proof should be > 20 KB, got {} bytes",
998            proof_bytes.len()
999        );
1000        assert!(
1001            proof_bytes.len() < MAX_PAYMENT_PROOF_SIZE_BYTES,
1002            "Real 5-quote ML-DSA proof ({} bytes) should fit within {} byte limit",
1003            proof_bytes.len(),
1004            MAX_PAYMENT_PROOF_SIZE_BYTES
1005        );
1006    }
1007
1008    #[tokio::test]
1009    async fn test_content_address_mismatch_rejected() {
1010        use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1011        use ant_evm::{EncodedPeerId, PaymentQuote, QuotingMetrics, RewardsAddress};
1012        use libp2p::identity::Keypair;
1013        use libp2p::PeerId;
1014        use std::time::SystemTime;
1015
1016        let verifier = create_test_verifier();
1017
1018        // The xorname we're trying to store
1019        let target_xorname = [0xAAu8; 32];
1020
1021        // Create a quote for a DIFFERENT xorname
1022        let wrong_xorname = [0xBBu8; 32];
1023        let quote = PaymentQuote {
1024            content: xor_name::XorName(wrong_xorname),
1025            timestamp: SystemTime::now(),
1026            quoting_metrics: QuotingMetrics {
1027                data_size: 1024,
1028                data_type: 0,
1029                close_records_stored: 0,
1030                records_per_type: vec![],
1031                max_records: 1000,
1032                received_payment_count: 0,
1033                live_time: 0,
1034                network_density: None,
1035                network_size: None,
1036            },
1037            rewards_address: RewardsAddress::new([1u8; 20]),
1038            pub_key: vec![0u8; 64],
1039            signature: vec![0u8; 64],
1040        };
1041
1042        // Build 5 quotes with distinct peer IDs (required by REQUIRED_QUOTES enforcement)
1043        let mut peer_quotes = Vec::new();
1044        for _ in 0..5 {
1045            let keypair = Keypair::generate_ed25519();
1046            let peer_id = PeerId::from_public_key(&keypair.public());
1047            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1048        }
1049
1050        let proof = PaymentProof {
1051            proof_of_payment: ProofOfPayment { peer_quotes },
1052            tx_hashes: vec![],
1053        };
1054
1055        let proof_bytes = serialize_single_node_proof(&proof).expect("serialize proof");
1056
1057        let result = verifier
1058            .verify_payment(&target_xorname, Some(&proof_bytes))
1059            .await;
1060
1061        assert!(result.is_err(), "Should reject mismatched content address");
1062        let err_msg = format!("{}", result.expect_err("should be error"));
1063        assert!(
1064            err_msg.contains("content address mismatch"),
1065            "Error should mention 'content address mismatch': {err_msg}"
1066        );
1067    }
1068
1069    /// Helper: create a fake quote with the given xorname and timestamp.
1070    fn make_fake_quote(
1071        xorname: [u8; 32],
1072        timestamp: SystemTime,
1073        rewards_address: RewardsAddress,
1074    ) -> ant_evm::PaymentQuote {
1075        use ant_evm::{PaymentQuote, QuotingMetrics};
1076
1077        PaymentQuote {
1078            content: xor_name::XorName(xorname),
1079            timestamp,
1080            quoting_metrics: QuotingMetrics {
1081                data_size: 1024,
1082                data_type: 0,
1083                close_records_stored: 0,
1084                records_per_type: vec![],
1085                max_records: 1000,
1086                received_payment_count: 0,
1087                live_time: 0,
1088                network_density: None,
1089                network_size: None,
1090            },
1091            rewards_address,
1092            pub_key: vec![0u8; 64],
1093            signature: vec![0u8; 64],
1094        }
1095    }
1096
1097    /// Helper: wrap quotes into a tagged serialized `PaymentProof`.
1098    fn serialize_proof(
1099        peer_quotes: Vec<(ant_evm::EncodedPeerId, ant_evm::PaymentQuote)>,
1100    ) -> Vec<u8> {
1101        use crate::payment::proof::{serialize_single_node_proof, PaymentProof};
1102
1103        let proof = PaymentProof {
1104            proof_of_payment: ProofOfPayment { peer_quotes },
1105            tx_hashes: vec![],
1106        };
1107        serialize_single_node_proof(&proof).expect("serialize proof")
1108    }
1109
1110    #[tokio::test]
1111    async fn test_expired_quote_rejected() {
1112        use ant_evm::{EncodedPeerId, RewardsAddress};
1113        use std::time::Duration;
1114
1115        let verifier = create_test_verifier();
1116        let xorname = [0xCCu8; 32];
1117        let rewards_addr = RewardsAddress::new([1u8; 20]);
1118
1119        // Create a quote that's 25 hours old (exceeds 24-hour max)
1120        let old_timestamp = SystemTime::now() - Duration::from_secs(25 * 3600);
1121        let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1122
1123        let mut peer_quotes = Vec::new();
1124        for _ in 0..5 {
1125            let keypair = libp2p::identity::Keypair::generate_ed25519();
1126            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1127            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1128        }
1129
1130        let proof_bytes = serialize_proof(peer_quotes);
1131        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1132
1133        assert!(result.is_err(), "Should reject expired quote");
1134        let err_msg = format!("{}", result.expect_err("should fail"));
1135        assert!(
1136            err_msg.contains("expired"),
1137            "Error should mention 'expired': {err_msg}"
1138        );
1139    }
1140
1141    #[tokio::test]
1142    async fn test_future_timestamp_rejected() {
1143        use ant_evm::{EncodedPeerId, RewardsAddress};
1144        use std::time::Duration;
1145
1146        let verifier = create_test_verifier();
1147        let xorname = [0xDDu8; 32];
1148        let rewards_addr = RewardsAddress::new([1u8; 20]);
1149
1150        // Create a quote with a timestamp 1 hour in the future
1151        let future_timestamp = SystemTime::now() + Duration::from_secs(3600);
1152        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1153
1154        let mut peer_quotes = Vec::new();
1155        for _ in 0..5 {
1156            let keypair = libp2p::identity::Keypair::generate_ed25519();
1157            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1158            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1159        }
1160
1161        let proof_bytes = serialize_proof(peer_quotes);
1162        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1163
1164        assert!(result.is_err(), "Should reject future-timestamped quote");
1165        let err_msg = format!("{}", result.expect_err("should fail"));
1166        assert!(
1167            err_msg.contains("future"),
1168            "Error should mention 'future': {err_msg}"
1169        );
1170    }
1171
1172    #[tokio::test]
1173    async fn test_quote_within_clock_skew_tolerance_accepted() {
1174        use ant_evm::{EncodedPeerId, RewardsAddress};
1175        use std::time::Duration;
1176
1177        let verifier = create_test_verifier();
1178        let xorname = [0xD1u8; 32];
1179        let rewards_addr = RewardsAddress::new([1u8; 20]);
1180
1181        // Quote 30 seconds in the future — within 60s tolerance
1182        let future_timestamp = SystemTime::now() + Duration::from_secs(30);
1183        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1184
1185        let mut peer_quotes = Vec::new();
1186        for _ in 0..5 {
1187            let keypair = libp2p::identity::Keypair::generate_ed25519();
1188            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1189            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1190        }
1191
1192        let proof_bytes = serialize_proof(peer_quotes);
1193        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1194
1195        // Should NOT fail at timestamp check (will fail later at pub_key binding)
1196        let err_msg = format!("{}", result.expect_err("should fail at later check"));
1197        assert!(
1198            !err_msg.contains("future"),
1199            "Should pass timestamp check (within tolerance), but got: {err_msg}"
1200        );
1201    }
1202
1203    #[tokio::test]
1204    async fn test_quote_just_beyond_clock_skew_tolerance_rejected() {
1205        use ant_evm::{EncodedPeerId, RewardsAddress};
1206        use std::time::Duration;
1207
1208        let verifier = create_test_verifier();
1209        let xorname = [0xD2u8; 32];
1210        let rewards_addr = RewardsAddress::new([1u8; 20]);
1211
1212        // Quote 120 seconds in the future — exceeds 60s tolerance
1213        let future_timestamp = SystemTime::now() + Duration::from_secs(120);
1214        let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);
1215
1216        let mut peer_quotes = Vec::new();
1217        for _ in 0..5 {
1218            let keypair = libp2p::identity::Keypair::generate_ed25519();
1219            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1220            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1221        }
1222
1223        let proof_bytes = serialize_proof(peer_quotes);
1224        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1225
1226        assert!(
1227            result.is_err(),
1228            "Should reject quote beyond clock skew tolerance"
1229        );
1230        let err_msg = format!("{}", result.expect_err("should fail"));
1231        assert!(
1232            err_msg.contains("future"),
1233            "Error should mention 'future': {err_msg}"
1234        );
1235    }
1236
1237    #[tokio::test]
1238    async fn test_quote_23h_old_still_accepted() {
1239        use ant_evm::{EncodedPeerId, RewardsAddress};
1240        use std::time::Duration;
1241
1242        let verifier = create_test_verifier();
1243        let xorname = [0xD3u8; 32];
1244        let rewards_addr = RewardsAddress::new([1u8; 20]);
1245
1246        // Quote 23 hours old — within 24h max age
1247        let old_timestamp = SystemTime::now() - Duration::from_secs(23 * 3600);
1248        let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);
1249
1250        let mut peer_quotes = Vec::new();
1251        for _ in 0..5 {
1252            let keypair = libp2p::identity::Keypair::generate_ed25519();
1253            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1254            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1255        }
1256
1257        let proof_bytes = serialize_proof(peer_quotes);
1258        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1259
1260        // Should NOT fail at timestamp check (will fail later at pub_key binding)
1261        let err_msg = format!("{}", result.expect_err("should fail at later check"));
1262        assert!(
1263            !err_msg.contains("expired"),
1264            "Should pass expiry check (23h < 24h), but got: {err_msg}"
1265        );
1266    }
1267
1268    /// Helper: build an `EncodedPeerId` that matches the BLAKE3 hash of an ML-DSA public key.
1269    fn encoded_peer_id_for_pub_key(pub_key: &[u8]) -> ant_evm::EncodedPeerId {
1270        let ant_peer_id = peer_id_from_public_key_bytes(pub_key).expect("valid ML-DSA pub key");
1271        // Wrap raw 32-byte peer ID in identity multihash format: [0x00, length, ...bytes]
1272        let raw = ant_peer_id.as_bytes();
1273        let mut multihash_bytes = Vec::with_capacity(2 + raw.len());
1274        multihash_bytes.push(0x00); // identity multihash code
1275                                    // PeerId is always 32 bytes, safely fits in u8
1276        multihash_bytes.push(u8::try_from(raw.len()).unwrap_or(32));
1277        multihash_bytes.extend_from_slice(raw);
1278        let libp2p_peer_id =
1279            libp2p::PeerId::from_bytes(&multihash_bytes).expect("valid multihash peer ID");
1280        ant_evm::EncodedPeerId::from(libp2p_peer_id)
1281    }
1282
1283    #[tokio::test]
1284    async fn test_local_not_in_paid_set_rejected() {
1285        use ant_evm::RewardsAddress;
1286        use saorsa_core::MlDsa65;
1287        use saorsa_pqc::pqc::MlDsaOperations;
1288
1289        // Verifier with a local rewards address set
1290        let local_addr = RewardsAddress::new([0xAAu8; 20]);
1291        let config = PaymentVerifierConfig {
1292            evm: EvmVerifierConfig {
1293                network: EvmNetwork::ArbitrumOne,
1294            },
1295            cache_capacity: 100,
1296            local_rewards_address: local_addr,
1297        };
1298        let verifier = PaymentVerifier::new(config);
1299
1300        let xorname = [0xEEu8; 32];
1301        // Quotes pay a DIFFERENT rewards address
1302        let other_addr = RewardsAddress::new([0xBBu8; 20]);
1303
1304        // Use real ML-DSA keys so the pub_key→peer_id binding check passes
1305        let ml_dsa = MlDsa65::new();
1306        let mut peer_quotes = Vec::new();
1307        for _ in 0..5 {
1308            let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1309            let pub_key_bytes = public_key.as_bytes().to_vec();
1310            let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
1311
1312            let mut quote = make_fake_quote(xorname, SystemTime::now(), other_addr);
1313            quote.pub_key = pub_key_bytes;
1314
1315            peer_quotes.push((encoded, quote));
1316        }
1317
1318        let proof_bytes = serialize_proof(peer_quotes);
1319        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1320
1321        assert!(result.is_err(), "Should reject payment not addressed to us");
1322        let err_msg = format!("{}", result.expect_err("should fail"));
1323        assert!(
1324            err_msg.contains("does not include this node as a recipient"),
1325            "Error should mention recipient rejection: {err_msg}"
1326        );
1327    }
1328
1329    #[tokio::test]
1330    async fn test_wrong_peer_binding_rejected() {
1331        use ant_evm::{EncodedPeerId, RewardsAddress};
1332        use saorsa_core::MlDsa65;
1333        use saorsa_pqc::pqc::MlDsaOperations;
1334
1335        let verifier = create_test_verifier();
1336        let xorname = [0xFFu8; 32];
1337        let rewards_addr = RewardsAddress::new([1u8; 20]);
1338
1339        // Generate a real ML-DSA keypair so pub_key is valid
1340        let ml_dsa = MlDsa65::new();
1341        let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
1342        let pub_key_bytes = public_key.as_bytes().to_vec();
1343
1344        // Create a quote with a real pub_key but attach it to a random peer ID
1345        // whose identity multihash does NOT match BLAKE3(pub_key)
1346        let mut quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1347        quote.pub_key = pub_key_bytes;
1348
1349        // Use random ed25519 peer IDs — they won't match BLAKE3(pub_key)
1350        let mut peer_quotes = Vec::new();
1351        for _ in 0..5 {
1352            let keypair = libp2p::identity::Keypair::generate_ed25519();
1353            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1354            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1355        }
1356
1357        let proof_bytes = serialize_proof(peer_quotes);
1358        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
1359
1360        assert!(result.is_err(), "Should reject wrong peer binding");
1361        let err_msg = format!("{}", result.expect_err("should fail"));
1362        assert!(
1363            err_msg.contains("pub_key does not belong to claimed peer"),
1364            "Error should mention binding mismatch: {err_msg}"
1365        );
1366    }
1367
1368    // =========================================================================
1369    // Merkle-tagged proof tests
1370    // =========================================================================
1371
1372    #[tokio::test]
1373    async fn test_merkle_tagged_proof_invalid_data_rejected() {
1374        use crate::ant_protocol::PROOF_TAG_MERKLE;
1375
1376        let verifier = create_test_verifier();
1377        let xorname = [0xA1u8; 32];
1378
1379        // Build a merkle-tagged proof with garbage body.
1380        // The tag byte is correct but the body is not valid msgpack.
1381        let mut merkle_garbage = Vec::with_capacity(64);
1382        merkle_garbage.push(PROOF_TAG_MERKLE);
1383        merkle_garbage.extend_from_slice(&[0xAB; 63]);
1384
1385        let result = verifier
1386            .verify_payment(&xorname, Some(&merkle_garbage))
1387            .await;
1388
1389        assert!(
1390            result.is_err(),
1391            "Should reject merkle proof with invalid body"
1392        );
1393        let err_msg = format!("{}", result.expect_err("should fail"));
1394        assert!(
1395            err_msg.contains("deserialize") || err_msg.contains("merkle proof"),
1396            "Error should mention deserialization failure: {err_msg}"
1397        );
1398    }
1399
1400    #[tokio::test]
1401    async fn test_single_node_tagged_proof_deserialization() {
1402        use crate::payment::proof::serialize_single_node_proof;
1403        use ant_evm::{EncodedPeerId, RewardsAddress};
1404
1405        let verifier = create_test_verifier();
1406        let xorname = [0xA2u8; 32];
1407        let rewards_addr = RewardsAddress::new([1u8; 20]);
1408
1409        // Build a valid tagged single-node proof
1410        let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
1411        let mut peer_quotes = Vec::new();
1412        for _ in 0..5 {
1413            let keypair = libp2p::identity::Keypair::generate_ed25519();
1414            let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
1415            peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
1416        }
1417
1418        let proof = crate::payment::proof::PaymentProof {
1419            proof_of_payment: ProofOfPayment {
1420                peer_quotes: peer_quotes.clone(),
1421            },
1422            tx_hashes: vec![],
1423        };
1424
1425        let tagged_bytes = serialize_single_node_proof(&proof).expect("serialize tagged proof");
1426
1427        // detect_proof_type should identify it as SingleNode
1428        assert_eq!(
1429            crate::payment::proof::detect_proof_type(&tagged_bytes),
1430            Some(crate::payment::proof::ProofType::SingleNode)
1431        );
1432
1433        // verify_payment should process it through the single-node path.
1434        // It will fail at quote validation (fake pub_key), but we verify
1435        // it passes the deserialization stage by checking the error type.
1436        let result = verifier.verify_payment(&xorname, Some(&tagged_bytes)).await;
1437
1438        assert!(result.is_err(), "Should fail at quote validation stage");
1439        let err_msg = format!("{}", result.expect_err("should fail"));
1440        // It should NOT be a deserialization error — it should get further
1441        assert!(
1442            !err_msg.contains("deserialize"),
1443            "Should pass deserialization but fail later: {err_msg}"
1444        );
1445    }
1446
1447    #[test]
1448    fn test_pool_cache_insert_and_lookup() {
1449        use evmlib::merkle_batch_payment::PoolHash;
1450
1451        // Verify the pool_cache field exists and works correctly.
1452        // Insert a pool hash, then verify it's present on lookup.
1453        let verifier = create_test_verifier();
1454
1455        let pool_hash: PoolHash = [0xBBu8; 32];
1456        let payment_info = ant_evm::merkle_payments::OnChainPaymentInfo {
1457            depth: 4,
1458            merkle_payment_timestamp: 1_700_000_000,
1459            paid_node_addresses: vec![],
1460        };
1461
1462        // Insert into pool cache
1463        {
1464            let mut cache = verifier.pool_cache.lock();
1465            cache.put(pool_hash, payment_info);
1466        }
1467
1468        // First lookup — should find it
1469        {
1470            let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1471            assert!(found.is_some(), "Pool hash should be in cache after insert");
1472            let info = found.expect("cached info");
1473            assert_eq!(info.depth, 4);
1474            assert_eq!(info.merkle_payment_timestamp, 1_700_000_000);
1475        }
1476
1477        // Second lookup — same result (no double-query needed)
1478        {
1479            let found = verifier.pool_cache.lock().get(&pool_hash).cloned();
1480            assert!(
1481                found.is_some(),
1482                "Pool hash should still be in cache on second lookup"
1483            );
1484        }
1485
1486        // Different pool hash — should NOT be found
1487        let other_hash: PoolHash = [0xCCu8; 32];
1488        {
1489            let found = verifier.pool_cache.lock().get(&other_hash).cloned();
1490            assert!(found.is_none(), "Unknown pool hash should not be in cache");
1491        }
1492    }
1493}