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