saorsa_node/payment/
quote.rs

1//! Payment quote generation for saorsa-node.
2//!
3//! Generates `PaymentQuote` values that clients use to pay for data storage.
4//! Compatible with the autonomi payment system.
5//!
6//! NOTE: Quote generation requires integration with the node's signing
7//! capabilities from saorsa-core. This module provides the interface
8//! and will be fully integrated when the node is initialized.
9
10use crate::error::Result;
11use crate::payment::metrics::QuotingMetricsTracker;
12use ant_evm::{PaymentQuote, QuotingMetrics, RewardsAddress};
13use std::time::SystemTime;
14use tracing::debug;
15
16/// Content address type (32-byte `XorName`).
17pub type XorName = [u8; 32];
18
19/// Signing function type that takes bytes and returns a signature.
20pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
21
22/// Quote generator for creating payment quotes.
23///
24/// Uses the node's signing capabilities to sign quotes, which clients
25/// use to pay for storage on the Arbitrum network.
26pub struct QuoteGenerator {
27    /// The rewards address for receiving payments.
28    rewards_address: RewardsAddress,
29    /// Metrics tracker for quoting.
30    metrics_tracker: QuotingMetricsTracker,
31    /// Signing function provided by the node.
32    /// Takes bytes and returns a signature.
33    sign_fn: Option<SignFn>,
34    /// Public key bytes for the quote.
35    pub_key: Vec<u8>,
36}
37
38impl QuoteGenerator {
39    /// Create a new quote generator without signing capability.
40    ///
41    /// Call `set_signer` to enable quote signing.
42    ///
43    /// # Arguments
44    ///
45    /// * `rewards_address` - The EVM address for receiving payments
46    /// * `metrics_tracker` - Tracker for quoting metrics
47    #[must_use]
48    pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
49        Self {
50            rewards_address,
51            metrics_tracker,
52            sign_fn: None,
53            pub_key: Vec::new(),
54        }
55    }
56
57    /// Set the signing function for quote generation.
58    ///
59    /// # Arguments
60    ///
61    /// * `pub_key` - The node's public key bytes
62    /// * `sign_fn` - Function that signs bytes and returns signature
63    pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
64    where
65        F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
66    {
67        self.pub_key = pub_key;
68        self.sign_fn = Some(Box::new(sign_fn));
69    }
70
71    /// Check if the generator has signing capability.
72    #[must_use]
73    pub fn can_sign(&self) -> bool {
74        self.sign_fn.is_some()
75    }
76
77    /// Generate a payment quote for storing data.
78    ///
79    /// # Arguments
80    ///
81    /// * `content` - The `XorName` of the content to store
82    /// * `data_size` - Size of the data in bytes
83    /// * `data_type` - Type index of the data (0 for chunks)
84    ///
85    /// # Returns
86    ///
87    /// A signed `PaymentQuote` that the client can use to pay on-chain.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if signing is not configured.
92    pub fn create_quote(
93        &self,
94        content: XorName,
95        data_size: usize,
96        data_type: u32,
97    ) -> Result<PaymentQuote> {
98        let sign_fn = self.sign_fn.as_ref().ok_or_else(|| {
99            crate::error::Error::Payment("Quote signing not configured".to_string())
100        })?;
101
102        let timestamp = SystemTime::now();
103
104        // Get current quoting metrics
105        let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
106
107        // Convert XorName to xor_name::XorName
108        let xor_name = xor_name::XorName(content);
109
110        // Create bytes for signing (following autonomi's pattern)
111        let bytes = PaymentQuote::bytes_for_signing(
112            xor_name,
113            timestamp,
114            &quoting_metrics,
115            &self.rewards_address,
116        );
117
118        // Sign the bytes
119        let signature = sign_fn(&bytes);
120
121        let quote = PaymentQuote {
122            content: xor_name,
123            timestamp,
124            quoting_metrics,
125            pub_key: self.pub_key.clone(),
126            rewards_address: self.rewards_address,
127            signature,
128        };
129
130        debug!(
131            "Generated quote for {} (size: {}, type: {})",
132            hex::encode(content),
133            data_size,
134            data_type
135        );
136
137        Ok(quote)
138    }
139
140    /// Get the rewards address.
141    #[must_use]
142    pub fn rewards_address(&self) -> &RewardsAddress {
143        &self.rewards_address
144    }
145
146    /// Get current quoting metrics.
147    #[must_use]
148    pub fn current_metrics(&self) -> QuotingMetrics {
149        self.metrics_tracker.get_metrics(0, 0)
150    }
151
152    /// Record a payment received (delegates to metrics tracker).
153    pub fn record_payment(&self) {
154        self.metrics_tracker.record_payment();
155    }
156
157    /// Record data stored (delegates to metrics tracker).
158    pub fn record_store(&self, data_type: u32) {
159        self.metrics_tracker.record_store(data_type);
160    }
161}
162
163/// Verify a payment quote signature.
164///
165/// # Arguments
166///
167/// * `quote` - The quote to verify
168/// * `expected_content` - The expected content `XorName`
169///
170/// # Returns
171///
172/// `true` if the content matches (signature verification requires public key).
173#[must_use]
174pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
175    // Check content matches
176    if quote.content.0 != *expected_content {
177        debug!(
178            "Quote content mismatch: expected {}, got {}",
179            hex::encode(expected_content),
180            hex::encode(quote.content.0)
181        );
182        return false;
183    }
184    true
185}
186
187#[cfg(test)]
188#[allow(clippy::expect_used)]
189mod tests {
190    use super::*;
191    use crate::payment::metrics::QuotingMetricsTracker;
192
193    fn create_test_generator() -> QuoteGenerator {
194        let rewards_address = RewardsAddress::new([1u8; 20]);
195        let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
196
197        let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
198
199        // Set up a dummy signer for testing
200        generator.set_signer(vec![0u8; 64], |bytes| {
201            // Dummy signature - just return hash of bytes
202            let mut sig = vec![0u8; 64];
203            for (i, b) in bytes.iter().take(64).enumerate() {
204                sig[i] = *b;
205            }
206            sig
207        });
208
209        generator
210    }
211
212    #[test]
213    fn test_create_quote() {
214        let generator = create_test_generator();
215        let content = [42u8; 32];
216
217        let quote = generator.create_quote(content, 1024, 0);
218        assert!(quote.is_ok());
219
220        let quote = quote.expect("valid quote");
221        assert_eq!(quote.content.0, content);
222    }
223
224    #[test]
225    fn test_verify_quote_content() {
226        let generator = create_test_generator();
227        let content = [42u8; 32];
228
229        let quote = generator
230            .create_quote(content, 1024, 0)
231            .expect("valid quote");
232        assert!(verify_quote_content(&quote, &content));
233
234        // Wrong content should fail
235        let wrong_content = [99u8; 32];
236        assert!(!verify_quote_content(&quote, &wrong_content));
237    }
238
239    #[test]
240    fn test_generator_without_signer() {
241        let rewards_address = RewardsAddress::new([1u8; 20]);
242        let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
243        let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
244
245        assert!(!generator.can_sign());
246
247        let content = [42u8; 32];
248        let result = generator.create_quote(content, 1024, 0);
249        assert!(result.is_err());
250    }
251}