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