1use crate::error::{Error, Result};
11use crate::payment::metrics::QuotingMetricsTracker;
12use ant_evm::{PaymentQuote, QuotingMetrics, RewardsAddress};
13use saorsa_core::MlDsa65;
14use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
15use saorsa_pqc::pqc::MlDsaOperations;
16use std::time::SystemTime;
17use tracing::debug;
18
19pub type XorName = [u8; 32];
21
22pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
24
25pub struct QuoteGenerator {
30 rewards_address: RewardsAddress,
32 metrics_tracker: QuotingMetricsTracker,
34 sign_fn: Option<SignFn>,
37 pub_key: Vec<u8>,
39}
40
41impl QuoteGenerator {
42 #[must_use]
51 pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
52 Self {
53 rewards_address,
54 metrics_tracker,
55 sign_fn: None,
56 pub_key: Vec::new(),
57 }
58 }
59
60 pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
67 where
68 F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
69 {
70 self.pub_key = pub_key;
71 self.sign_fn = Some(Box::new(sign_fn));
72 }
73
74 #[must_use]
76 pub fn can_sign(&self) -> bool {
77 self.sign_fn.is_some()
78 }
79
80 pub fn probe_signer(&self) -> Result<()> {
86 let sign_fn = self
87 .sign_fn
88 .as_ref()
89 .ok_or_else(|| Error::Payment("Signer not set".to_string()))?;
90 let test_msg = b"saorsa-signing-probe";
91 let test_sig = sign_fn(test_msg);
92 if test_sig.is_empty() {
93 return Err(Error::Payment(
94 "ML-DSA-65 signing probe failed: empty signature produced".to_string(),
95 ));
96 }
97 Ok(())
98 }
99
100 pub fn create_quote(
116 &self,
117 content: XorName,
118 data_size: usize,
119 data_type: u32,
120 ) -> Result<PaymentQuote> {
121 let sign_fn = self
122 .sign_fn
123 .as_ref()
124 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
125
126 let timestamp = SystemTime::now();
127
128 let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
130
131 let xor_name = xor_name::XorName(content);
133
134 let bytes = PaymentQuote::bytes_for_signing(
136 xor_name,
137 timestamp,
138 "ing_metrics,
139 &self.rewards_address,
140 );
141
142 let signature = sign_fn(&bytes);
144 if signature.is_empty() {
145 return Err(Error::Payment(
146 "Signing produced empty signature".to_string(),
147 ));
148 }
149
150 let quote = PaymentQuote {
151 content: xor_name,
152 timestamp,
153 quoting_metrics,
154 pub_key: self.pub_key.clone(),
155 rewards_address: self.rewards_address,
156 signature,
157 };
158
159 if tracing::enabled!(tracing::Level::DEBUG) {
160 let content_hex = hex::encode(content);
161 debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})");
162 }
163
164 Ok(quote)
165 }
166
167 #[must_use]
169 pub fn rewards_address(&self) -> &RewardsAddress {
170 &self.rewards_address
171 }
172
173 #[must_use]
175 pub fn current_metrics(&self) -> QuotingMetrics {
176 self.metrics_tracker.get_metrics(0, 0)
177 }
178
179 pub fn record_payment(&self) {
181 self.metrics_tracker.record_payment();
182 }
183
184 pub fn record_store(&self, data_type: u32) {
186 self.metrics_tracker.record_store(data_type);
187 }
188}
189
190#[must_use]
201pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
202 if quote.content.0 != *expected_content {
204 if tracing::enabled!(tracing::Level::DEBUG) {
205 debug!(
206 "Quote content mismatch: expected {}, got {}",
207 hex::encode(expected_content),
208 hex::encode(quote.content.0)
209 );
210 }
211 return false;
212 }
213 true
214}
215
216#[must_use]
230pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
231 let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
233 Ok(pk) => pk,
234 Err(e) => {
235 debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
236 return false;
237 }
238 };
239
240 let signature = match MlDsaSignature::from_bytes("e.signature) {
242 Ok(sig) => sig,
243 Err(e) => {
244 debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
245 return false;
246 }
247 };
248
249 let bytes = quote.bytes_for_sig();
251
252 let ml_dsa = MlDsa65::new();
254 match ml_dsa.verify(&pub_key, &bytes, &signature) {
255 Ok(valid) => {
256 if !valid {
257 debug!("ML-DSA-65 quote signature verification failed");
258 }
259 valid
260 }
261 Err(e) => {
262 debug!("ML-DSA-65 verification error: {e}");
263 false
264 }
265 }
266}
267
268pub fn wire_ml_dsa_signer(
283 generator: &mut QuoteGenerator,
284 identity: &saorsa_core::identity::NodeIdentity,
285) -> Result<()> {
286 let pub_key_bytes = identity.public_key().as_bytes().to_vec();
287 let sk_bytes = identity.secret_key_bytes().to_vec();
288 let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
289 .map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
290 let ml_dsa = MlDsa65::new();
291 generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
292 Ok(sig) => sig.as_bytes().to_vec(),
293 Err(e) => {
294 tracing::error!("ML-DSA-65 signing failed: {e}");
295 vec![]
296 }
297 });
298 generator.probe_signer()?;
299 Ok(())
300}
301
302#[cfg(test)]
303#[allow(clippy::expect_used)]
304mod tests {
305 use super::*;
306 use crate::payment::metrics::QuotingMetricsTracker;
307 use saorsa_pqc::pqc::types::MlDsaSecretKey;
308
309 fn create_test_generator() -> QuoteGenerator {
310 let rewards_address = RewardsAddress::new([1u8; 20]);
311 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
312
313 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
314
315 generator.set_signer(vec![0u8; 64], |bytes| {
317 let mut sig = vec![0u8; 64];
319 for (i, b) in bytes.iter().take(64).enumerate() {
320 sig[i] = *b;
321 }
322 sig
323 });
324
325 generator
326 }
327
328 #[test]
329 fn test_create_quote() {
330 let generator = create_test_generator();
331 let content = [42u8; 32];
332
333 let quote = generator.create_quote(content, 1024, 0);
334 assert!(quote.is_ok());
335
336 let quote = quote.expect("valid quote");
337 assert_eq!(quote.content.0, content);
338 }
339
340 #[test]
341 fn test_verify_quote_content() {
342 let generator = create_test_generator();
343 let content = [42u8; 32];
344
345 let quote = generator
346 .create_quote(content, 1024, 0)
347 .expect("valid quote");
348 assert!(verify_quote_content("e, &content));
349
350 let wrong_content = [99u8; 32];
352 assert!(!verify_quote_content("e, &wrong_content));
353 }
354
355 #[test]
356 fn test_generator_without_signer() {
357 let rewards_address = RewardsAddress::new([1u8; 20]);
358 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
359 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
360
361 assert!(!generator.can_sign());
362
363 let content = [42u8; 32];
364 let result = generator.create_quote(content, 1024, 0);
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_quote_signature_round_trip_real_keys() {
370 let ml_dsa = MlDsa65::new();
371 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
372
373 let rewards_address = RewardsAddress::new([2u8; 20]);
374 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
375 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
376
377 let pub_key_bytes = public_key.as_bytes().to_vec();
378 let sk_bytes = secret_key.as_bytes().to_vec();
379 generator.set_signer(pub_key_bytes, move |msg| {
380 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
381 let ml_dsa = MlDsa65::new();
382 ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
383 });
384
385 let content = [7u8; 32];
386 let quote = generator
387 .create_quote(content, 2048, 0)
388 .expect("create quote");
389
390 assert!(verify_quote_signature("e));
392
393 let mut tampered_quote = quote;
395 if let Some(byte) = tampered_quote.signature.first_mut() {
396 *byte ^= 0xFF;
397 }
398 assert!(!verify_quote_signature(&tampered_quote));
399 }
400
401 #[test]
402 fn test_empty_signature_fails_verification() {
403 let generator = create_test_generator();
404 let content = [42u8; 32];
405
406 let quote = generator
407 .create_quote(content, 1024, 0)
408 .expect("create quote");
409
410 assert!(!verify_quote_signature("e));
413 }
414
415 #[test]
416 fn test_rewards_address_getter() {
417 let addr = RewardsAddress::new([42u8; 20]);
418 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
419 let generator = QuoteGenerator::new(addr, metrics_tracker);
420
421 assert_eq!(*generator.rewards_address(), addr);
422 }
423
424 #[test]
425 fn test_current_metrics() {
426 let rewards_address = RewardsAddress::new([1u8; 20]);
427 let metrics_tracker = QuotingMetricsTracker::new(500, 50);
428 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
429
430 let metrics = generator.current_metrics();
431 assert_eq!(metrics.max_records, 500);
432 assert_eq!(metrics.close_records_stored, 50);
433 assert_eq!(metrics.data_size, 0);
434 assert_eq!(metrics.data_type, 0);
435 }
436
437 #[test]
438 fn test_record_payment_delegation() {
439 let rewards_address = RewardsAddress::new([1u8; 20]);
440 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
441 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
442
443 generator.record_payment();
444 generator.record_payment();
445
446 let metrics = generator.current_metrics();
447 assert_eq!(metrics.received_payment_count, 2);
448 }
449
450 #[test]
451 fn test_record_store_delegation() {
452 let rewards_address = RewardsAddress::new([1u8; 20]);
453 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
454 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
455
456 generator.record_store(0);
457 generator.record_store(1);
458 generator.record_store(0);
459
460 let metrics = generator.current_metrics();
461 assert_eq!(metrics.close_records_stored, 3);
462 }
463
464 #[test]
465 fn test_create_quote_different_data_types() {
466 let generator = create_test_generator();
467 let content = [10u8; 32];
468
469 let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
471 assert_eq!(q0.quoting_metrics.data_type, 0);
472
473 let q1 = generator.create_quote(content, 512, 1).expect("type 1");
475 assert_eq!(q1.quoting_metrics.data_type, 1);
476
477 let q2 = generator.create_quote(content, 256, 2).expect("type 2");
479 assert_eq!(q2.quoting_metrics.data_type, 2);
480 }
481
482 #[test]
483 fn test_create_quote_zero_size() {
484 let generator = create_test_generator();
485 let content = [11u8; 32];
486
487 let quote = generator.create_quote(content, 0, 0).expect("zero size");
488 assert_eq!(quote.quoting_metrics.data_size, 0);
489 }
490
491 #[test]
492 fn test_create_quote_large_size() {
493 let generator = create_test_generator();
494 let content = [12u8; 32];
495
496 let quote = generator
497 .create_quote(content, 10_000_000, 0)
498 .expect("large size");
499 assert_eq!(quote.quoting_metrics.data_size, 10_000_000);
500 }
501
502 #[test]
503 fn test_verify_quote_signature_empty_pub_key() {
504 let quote = PaymentQuote {
505 content: xor_name::XorName([0u8; 32]),
506 timestamp: SystemTime::now(),
507 quoting_metrics: ant_evm::QuotingMetrics {
508 data_size: 0,
509 data_type: 0,
510 close_records_stored: 0,
511 records_per_type: vec![],
512 max_records: 0,
513 received_payment_count: 0,
514 live_time: 0,
515 network_density: None,
516 network_size: None,
517 },
518 rewards_address: RewardsAddress::new([0u8; 20]),
519 pub_key: vec![],
520 signature: vec![],
521 };
522
523 assert!(!verify_quote_signature("e));
525 }
526
527 #[test]
528 fn test_can_sign_after_set_signer() {
529 let rewards_address = RewardsAddress::new([1u8; 20]);
530 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
531 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
532
533 assert!(!generator.can_sign());
534
535 generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
536
537 assert!(generator.can_sign());
538 }
539
540 #[test]
541 fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
542 let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
543 let rewards_address = RewardsAddress::new([3u8; 20]);
544 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
545 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
546
547 let result = wire_ml_dsa_signer(&mut generator, &identity);
548 assert!(
549 result.is_ok(),
550 "wire_ml_dsa_signer should succeed: {result:?}"
551 );
552 assert!(generator.can_sign());
553 }
554
555 #[test]
556 fn test_probe_signer_fails_without_signer() {
557 let rewards_address = RewardsAddress::new([1u8; 20]);
558 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
559 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
560
561 let result = generator.probe_signer();
562 assert!(result.is_err());
563 }
564
565 #[test]
566 fn test_probe_signer_fails_with_empty_signature() {
567 let rewards_address = RewardsAddress::new([1u8; 20]);
568 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
569 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
570
571 generator.set_signer(vec![0u8; 32], |_| vec![]);
572
573 let result = generator.probe_signer();
574 assert!(result.is_err());
575 }
576}