Skip to main content

saorsa_node/payment/
verifier.rs

1//! Payment verifier with LRU cache and EVM verification.
2//!
3//! This is the core payment verification logic for saorsa-node.
4//! All new data requires EVM payment on Arbitrum (no free tier).
5
6use crate::error::{Error, Result};
7use crate::payment::cache::{VerifiedCache, XorName};
8use ant_evm::ProofOfPayment;
9use evmlib::Network as EvmNetwork;
10use tracing::{debug, info, warn};
11
12/// Configuration for EVM payment verification.
13#[derive(Debug, Clone)]
14pub struct EvmVerifierConfig {
15    /// EVM network to use (Arbitrum One, Arbitrum Sepolia, etc.)
16    pub network: EvmNetwork,
17    /// Whether EVM verification is enabled.
18    pub enabled: bool,
19}
20
21impl Default for EvmVerifierConfig {
22    fn default() -> Self {
23        Self {
24            network: EvmNetwork::ArbitrumOne,
25            enabled: true,
26        }
27    }
28}
29
30/// Configuration for the payment verifier.
31///
32/// All new data requires EVM payment on Arbitrum. The cache stores
33/// previously verified payments to avoid redundant on-chain lookups.
34#[derive(Debug, Clone)]
35pub struct PaymentVerifierConfig {
36    /// EVM verifier configuration.
37    pub evm: EvmVerifierConfig,
38    /// Cache capacity (number of `XorName` values to cache).
39    pub cache_capacity: usize,
40}
41
42impl Default for PaymentVerifierConfig {
43    fn default() -> Self {
44        Self {
45            evm: EvmVerifierConfig::default(),
46            cache_capacity: 100_000,
47        }
48    }
49}
50
51/// Status returned by payment verification.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PaymentStatus {
54    /// Data was found in local cache - previously paid.
55    CachedAsVerified,
56    /// New data - payment required.
57    PaymentRequired,
58    /// Payment was provided and verified.
59    PaymentVerified,
60}
61
62impl PaymentStatus {
63    /// Returns true if the data can be stored (cached or payment verified).
64    #[must_use]
65    pub fn can_store(&self) -> bool {
66        matches!(self, Self::CachedAsVerified | Self::PaymentVerified)
67    }
68
69    /// Returns true if this status indicates the data was already paid for.
70    #[must_use]
71    pub fn is_cached(&self) -> bool {
72        matches!(self, Self::CachedAsVerified)
73    }
74}
75
76/// Main payment verifier for saorsa-node.
77///
78/// Uses:
79/// 1. LRU cache for fast lookups of previously verified `XorName` values
80/// 2. EVM payment verification for new data (always required)
81pub struct PaymentVerifier {
82    /// LRU cache of verified `XorName` values.
83    cache: VerifiedCache,
84    /// Configuration.
85    config: PaymentVerifierConfig,
86}
87
88impl PaymentVerifier {
89    /// Create a new payment verifier.
90    #[must_use]
91    pub fn new(config: PaymentVerifierConfig) -> Self {
92        let cache = VerifiedCache::with_capacity(config.cache_capacity);
93
94        info!(
95            "Payment verifier initialized (cache_capacity={}, evm_enabled={})",
96            config.cache_capacity, config.evm.enabled
97        );
98
99        Self { cache, config }
100    }
101
102    /// Check if payment is required for the given `XorName`.
103    ///
104    /// This is the main entry point for payment verification:
105    /// 1. Check LRU cache (fast path)
106    /// 2. If not cached, payment is required
107    ///
108    /// # Arguments
109    ///
110    /// * `xorname` - The content-addressed name of the data
111    ///
112    /// # Returns
113    ///
114    /// * `PaymentStatus::CachedAsVerified` - Found in local cache (previously paid)
115    /// * `PaymentStatus::PaymentRequired` - Not cached (payment required)
116    pub fn check_payment_required(&self, xorname: &XorName) -> PaymentStatus {
117        // Check LRU cache (fast path)
118        if self.cache.contains(xorname) {
119            debug!("Data {} found in verified cache", hex::encode(xorname));
120            return PaymentStatus::CachedAsVerified;
121        }
122
123        // Not in cache - payment required
124        debug!(
125            "Data {} not in cache - payment required",
126            hex::encode(xorname)
127        );
128        PaymentStatus::PaymentRequired
129    }
130
131    /// Verify that a PUT request has valid payment.
132    ///
133    /// This is the complete payment verification flow:
134    /// 1. Check if data is in cache (previously paid)
135    /// 2. If not, verify the provided payment proof
136    ///
137    /// # Arguments
138    ///
139    /// * `xorname` - The content-addressed name of the data
140    /// * `payment_proof` - Optional payment proof (required if not in cache)
141    ///
142    /// # Returns
143    ///
144    /// * `Ok(PaymentStatus)` - Verification succeeded
145    /// * `Err(Error::Payment)` - No payment and not cached, or payment invalid
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if payment is required but not provided, or if payment is invalid.
150    pub async fn verify_payment(
151        &self,
152        xorname: &XorName,
153        payment_proof: Option<&[u8]>,
154    ) -> Result<PaymentStatus> {
155        // First check if payment is required
156        let status = self.check_payment_required(xorname);
157
158        match status {
159            PaymentStatus::CachedAsVerified => {
160                // No payment needed - already in cache
161                Ok(status)
162            }
163            PaymentStatus::PaymentRequired => {
164                // Payment is required - verify the proof
165                match payment_proof {
166                    Some(proof) => {
167                        if proof.is_empty() {
168                            return Err(Error::Payment("Empty payment proof".to_string()));
169                        }
170
171                        // Deserialize the ProofOfPayment
172                        let payment: ProofOfPayment =
173                            rmp_serde::from_slice(proof).map_err(|e| {
174                                Error::Payment(format!("Failed to deserialize payment proof: {e}"))
175                            })?;
176
177                        // Verify the payment using EVM
178                        self.verify_evm_payment(xorname, &payment).await?;
179
180                        // Cache the verified xorname
181                        self.cache.insert(*xorname);
182
183                        Ok(PaymentStatus::PaymentVerified)
184                    }
185                    None => {
186                        // No payment provided
187                        Err(Error::Payment(format!(
188                            "Payment required for new data {}",
189                            hex::encode(xorname)
190                        )))
191                    }
192                }
193            }
194            PaymentStatus::PaymentVerified => {
195                // This shouldn't happen from check_payment_required
196                Ok(status)
197            }
198        }
199    }
200
201    /// Get cache statistics.
202    #[must_use]
203    pub fn cache_stats(&self) -> crate::payment::cache::CacheStats {
204        self.cache.stats()
205    }
206
207    /// Get the number of cached entries.
208    #[must_use]
209    pub fn cache_len(&self) -> usize {
210        self.cache.len()
211    }
212
213    /// Check if EVM verification is enabled.
214    #[must_use]
215    pub fn evm_enabled(&self) -> bool {
216        self.config.evm.enabled
217    }
218
219    /// Verify an EVM payment proof.
220    ///
221    /// This verifies that:
222    /// 1. All quote signatures are valid
223    /// 2. The payment was made on-chain
224    async fn verify_evm_payment(&self, xorname: &XorName, payment: &ProofOfPayment) -> Result<()> {
225        debug!(
226            "Verifying EVM payment for {} with {} quotes",
227            hex::encode(xorname),
228            payment.peer_quotes.len()
229        );
230
231        // Skip EVM verification if disabled
232        if !self.config.evm.enabled {
233            warn!("EVM verification disabled - accepting payment without on-chain check");
234            return Ok(());
235        }
236
237        // Verify quote signatures first (doesn't require network)
238        for (encoded_peer_id, quote) in &payment.peer_quotes {
239            let peer_id = encoded_peer_id
240                .to_peer_id()
241                .map_err(|e| Error::Payment(format!("Invalid peer ID in payment proof: {e}")))?;
242
243            if !quote.check_is_signed_by_claimed_peer(peer_id) {
244                return Err(Error::Payment(format!(
245                    "Quote signature invalid for peer {peer_id}"
246                )));
247            }
248        }
249
250        // Get the payment digest for on-chain verification
251        let payment_digest = payment.digest();
252
253        if payment_digest.is_empty() {
254            return Err(Error::Payment("Payment has no quotes".to_string()));
255        }
256
257        // Verify on-chain payment
258        // Note: We pass empty owned_quote_hashes because we're not a node claiming payment,
259        // we just want to verify the payment is valid
260        let owned_quote_hashes = vec![];
261        match evmlib::contract::payment_vault::verify_data_payment(
262            &self.config.evm.network,
263            owned_quote_hashes,
264            payment_digest,
265        )
266        .await
267        {
268            Ok(_amount) => {
269                info!("EVM payment verified for {}", hex::encode(xorname));
270                Ok(())
271            }
272            Err(evmlib::contract::payment_vault::error::Error::PaymentInvalid) => {
273                Err(Error::Payment(format!(
274                    "Payment verification failed on-chain for {}",
275                    hex::encode(xorname)
276                )))
277            }
278            Err(e) => Err(Error::Payment(format!(
279                "EVM verification error for {}: {e}",
280                hex::encode(xorname)
281            ))),
282        }
283    }
284}
285
286#[cfg(test)]
287#[allow(clippy::expect_used)]
288mod tests {
289    use super::*;
290
291    fn create_test_verifier() -> PaymentVerifier {
292        let config = PaymentVerifierConfig {
293            evm: EvmVerifierConfig {
294                enabled: false, // Disabled for tests
295                ..Default::default()
296            },
297            cache_capacity: 100,
298        };
299        PaymentVerifier::new(config)
300    }
301
302    #[test]
303    fn test_payment_required_for_new_data() {
304        let verifier = create_test_verifier();
305        let xorname = [1u8; 32];
306
307        // All uncached data requires payment
308        let status = verifier.check_payment_required(&xorname);
309        assert_eq!(status, PaymentStatus::PaymentRequired);
310    }
311
312    #[test]
313    fn test_cache_hit() {
314        let verifier = create_test_verifier();
315        let xorname = [1u8; 32];
316
317        // Manually add to cache
318        verifier.cache.insert(xorname);
319
320        // Should return CachedAsVerified
321        let status = verifier.check_payment_required(&xorname);
322        assert_eq!(status, PaymentStatus::CachedAsVerified);
323    }
324
325    #[tokio::test]
326    async fn test_verify_payment_without_proof() {
327        let verifier = create_test_verifier();
328        let xorname = [1u8; 32];
329
330        // Should fail without payment proof
331        let result = verifier.verify_payment(&xorname, None).await;
332        assert!(result.is_err());
333    }
334
335    #[tokio::test]
336    async fn test_verify_payment_with_proof() {
337        let verifier = create_test_verifier();
338        let xorname = [1u8; 32];
339
340        // Create a valid (but empty) ProofOfPayment
341        let proof = ProofOfPayment {
342            peer_quotes: vec![],
343        };
344        let proof_bytes = rmp_serde::to_vec(&proof).expect("should serialize");
345
346        // Should succeed with a valid proof when EVM verification is disabled
347        // Note: With EVM verification disabled, even empty proofs pass
348        let result = verifier.verify_payment(&xorname, Some(&proof_bytes)).await;
349        assert!(result.is_ok(), "Expected Ok, got: {result:?}");
350        assert_eq!(result.expect("verified"), PaymentStatus::PaymentVerified);
351    }
352
353    #[tokio::test]
354    async fn test_verify_payment_cached() {
355        let verifier = create_test_verifier();
356        let xorname = [1u8; 32];
357
358        // Add to cache
359        verifier.cache.insert(xorname);
360
361        // Should succeed without payment (cached)
362        let result = verifier.verify_payment(&xorname, None).await;
363        assert!(result.is_ok());
364        assert_eq!(result.expect("cached"), PaymentStatus::CachedAsVerified);
365    }
366
367    #[test]
368    fn test_payment_status_can_store() {
369        assert!(PaymentStatus::CachedAsVerified.can_store());
370        assert!(PaymentStatus::PaymentVerified.can_store());
371        assert!(!PaymentStatus::PaymentRequired.can_store());
372    }
373
374    #[test]
375    fn test_payment_status_is_cached() {
376        assert!(PaymentStatus::CachedAsVerified.is_cached());
377        assert!(!PaymentStatus::PaymentVerified.is_cached());
378        assert!(!PaymentStatus::PaymentRequired.is_cached());
379    }
380}