1use crate::error::{Error, Result};
11use crate::payment::metrics::QuotingMetricsTracker;
12use ant_evm::merkle_payments::MerklePaymentCandidateNode;
13use ant_evm::{PaymentQuote, QuotingMetrics, RewardsAddress};
14use saorsa_core::MlDsa65;
15use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
16use saorsa_pqc::pqc::MlDsaOperations;
17use std::time::SystemTime;
18use tracing::debug;
19
20pub type XorName = [u8; 32];
22
23pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
25
26pub struct QuoteGenerator {
31 rewards_address: RewardsAddress,
33 metrics_tracker: QuotingMetricsTracker,
35 sign_fn: Option<SignFn>,
38 pub_key: Vec<u8>,
40}
41
42impl QuoteGenerator {
43 #[must_use]
52 pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
53 Self {
54 rewards_address,
55 metrics_tracker,
56 sign_fn: None,
57 pub_key: Vec::new(),
58 }
59 }
60
61 pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
68 where
69 F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
70 {
71 self.pub_key = pub_key;
72 self.sign_fn = Some(Box::new(sign_fn));
73 }
74
75 #[must_use]
77 pub fn can_sign(&self) -> bool {
78 self.sign_fn.is_some()
79 }
80
81 pub fn probe_signer(&self) -> Result<()> {
87 let sign_fn = self
88 .sign_fn
89 .as_ref()
90 .ok_or_else(|| Error::Payment("Signer not set".to_string()))?;
91 let test_msg = b"ant-signing-probe";
92 let test_sig = sign_fn(test_msg);
93 if test_sig.is_empty() {
94 return Err(Error::Payment(
95 "ML-DSA-65 signing probe failed: empty signature produced".to_string(),
96 ));
97 }
98 Ok(())
99 }
100
101 pub fn create_quote(
117 &self,
118 content: XorName,
119 data_size: usize,
120 data_type: u32,
121 ) -> Result<PaymentQuote> {
122 let sign_fn = self
123 .sign_fn
124 .as_ref()
125 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
126
127 let timestamp = SystemTime::now();
128
129 let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
131
132 let xor_name = xor_name::XorName(content);
134
135 let bytes = PaymentQuote::bytes_for_signing(
137 xor_name,
138 timestamp,
139 "ing_metrics,
140 &self.rewards_address,
141 );
142
143 let signature = sign_fn(&bytes);
145 if signature.is_empty() {
146 return Err(Error::Payment(
147 "Signing produced empty signature".to_string(),
148 ));
149 }
150
151 let quote = PaymentQuote {
152 content: xor_name,
153 timestamp,
154 quoting_metrics,
155 pub_key: self.pub_key.clone(),
156 rewards_address: self.rewards_address,
157 signature,
158 };
159
160 if tracing::enabled!(tracing::Level::DEBUG) {
161 let content_hex = hex::encode(content);
162 debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})");
163 }
164
165 Ok(quote)
166 }
167
168 #[must_use]
170 pub fn rewards_address(&self) -> &RewardsAddress {
171 &self.rewards_address
172 }
173
174 #[must_use]
176 pub fn current_metrics(&self) -> QuotingMetrics {
177 self.metrics_tracker.get_metrics(0, 0)
178 }
179
180 pub fn record_payment(&self) {
182 self.metrics_tracker.record_payment();
183 }
184
185 pub fn record_store(&self, data_type: u32) {
187 self.metrics_tracker.record_store(data_type);
188 }
189
190 pub fn create_merkle_candidate_quote(
205 &self,
206 data_size: usize,
207 data_type: u32,
208 merkle_payment_timestamp: u64,
209 ) -> Result<MerklePaymentCandidateNode> {
210 let sign_fn = self
211 .sign_fn
212 .as_ref()
213 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
214
215 let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
216
217 let msg = MerklePaymentCandidateNode::bytes_to_sign(
219 "ing_metrics,
220 &self.rewards_address,
221 merkle_payment_timestamp,
222 );
223
224 let signature = sign_fn(&msg);
226 if signature.is_empty() {
227 return Err(Error::Payment(
228 "ML-DSA-65 signing produced empty signature for merkle candidate".to_string(),
229 ));
230 }
231
232 let candidate = MerklePaymentCandidateNode {
233 pub_key: self.pub_key.clone(),
234 quoting_metrics,
235 reward_address: self.rewards_address,
236 merkle_payment_timestamp,
237 signature,
238 };
239
240 if tracing::enabled!(tracing::Level::DEBUG) {
241 debug!(
242 "Generated ML-DSA-65 merkle candidate quote (size: {data_size}, type: {data_type}, ts: {merkle_payment_timestamp})"
243 );
244 }
245
246 Ok(candidate)
247 }
248}
249
250#[must_use]
261pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
262 if quote.content.0 != *expected_content {
264 if tracing::enabled!(tracing::Level::DEBUG) {
265 debug!(
266 "Quote content mismatch: expected {}, got {}",
267 hex::encode(expected_content),
268 hex::encode(quote.content.0)
269 );
270 }
271 return false;
272 }
273 true
274}
275
276#[must_use]
290pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
291 let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
293 Ok(pk) => pk,
294 Err(e) => {
295 debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
296 return false;
297 }
298 };
299
300 let signature = match MlDsaSignature::from_bytes("e.signature) {
302 Ok(sig) => sig,
303 Err(e) => {
304 debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
305 return false;
306 }
307 };
308
309 let bytes = quote.bytes_for_sig();
311
312 let ml_dsa = MlDsa65::new();
314 match ml_dsa.verify(&pub_key, &bytes, &signature) {
315 Ok(valid) => {
316 if !valid {
317 debug!("ML-DSA-65 quote signature verification failed");
318 }
319 valid
320 }
321 Err(e) => {
322 debug!("ML-DSA-65 verification error: {e}");
323 false
324 }
325 }
326}
327
328#[must_use]
338pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
339 let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
340 Ok(pk) => pk,
341 Err(e) => {
342 debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
343 return false;
344 }
345 };
346
347 let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
348 Ok(sig) => sig,
349 Err(e) => {
350 debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
351 return false;
352 }
353 };
354
355 let msg = MerklePaymentCandidateNode::bytes_to_sign(
356 &candidate.quoting_metrics,
357 &candidate.reward_address,
358 candidate.merkle_payment_timestamp,
359 );
360
361 let ml_dsa = MlDsa65::new();
362 match ml_dsa.verify(&pub_key, &msg, &signature) {
363 Ok(valid) => {
364 if !valid {
365 debug!("ML-DSA-65 merkle candidate signature verification failed");
366 }
367 valid
368 }
369 Err(e) => {
370 debug!("ML-DSA-65 merkle candidate verification error: {e}");
371 false
372 }
373 }
374}
375
376pub fn wire_ml_dsa_signer(
391 generator: &mut QuoteGenerator,
392 identity: &saorsa_core::identity::NodeIdentity,
393) -> Result<()> {
394 let pub_key_bytes = identity.public_key().as_bytes().to_vec();
395 let sk_bytes = identity.secret_key_bytes().to_vec();
396 let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
397 .map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
398 let ml_dsa = MlDsa65::new();
399 generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
400 Ok(sig) => sig.as_bytes().to_vec(),
401 Err(e) => {
402 tracing::error!("ML-DSA-65 signing failed: {e}");
403 vec![]
404 }
405 });
406 generator.probe_signer()?;
407 Ok(())
408}
409
410#[cfg(test)]
411#[allow(clippy::expect_used)]
412mod tests {
413 use super::*;
414 use crate::payment::metrics::QuotingMetricsTracker;
415 use saorsa_pqc::pqc::types::MlDsaSecretKey;
416
417 fn create_test_generator() -> QuoteGenerator {
418 let rewards_address = RewardsAddress::new([1u8; 20]);
419 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
420
421 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
422
423 generator.set_signer(vec![0u8; 64], |bytes| {
425 let mut sig = vec![0u8; 64];
427 for (i, b) in bytes.iter().take(64).enumerate() {
428 sig[i] = *b;
429 }
430 sig
431 });
432
433 generator
434 }
435
436 #[test]
437 fn test_create_quote() {
438 let generator = create_test_generator();
439 let content = [42u8; 32];
440
441 let quote = generator.create_quote(content, 1024, 0);
442 assert!(quote.is_ok());
443
444 let quote = quote.expect("valid quote");
445 assert_eq!(quote.content.0, content);
446 }
447
448 #[test]
449 fn test_verify_quote_content() {
450 let generator = create_test_generator();
451 let content = [42u8; 32];
452
453 let quote = generator
454 .create_quote(content, 1024, 0)
455 .expect("valid quote");
456 assert!(verify_quote_content("e, &content));
457
458 let wrong_content = [99u8; 32];
460 assert!(!verify_quote_content("e, &wrong_content));
461 }
462
463 #[test]
464 fn test_generator_without_signer() {
465 let rewards_address = RewardsAddress::new([1u8; 20]);
466 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
467 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
468
469 assert!(!generator.can_sign());
470
471 let content = [42u8; 32];
472 let result = generator.create_quote(content, 1024, 0);
473 assert!(result.is_err());
474 }
475
476 #[test]
477 fn test_quote_signature_round_trip_real_keys() {
478 let ml_dsa = MlDsa65::new();
479 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
480
481 let rewards_address = RewardsAddress::new([2u8; 20]);
482 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
483 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
484
485 let pub_key_bytes = public_key.as_bytes().to_vec();
486 let sk_bytes = secret_key.as_bytes().to_vec();
487 generator.set_signer(pub_key_bytes, move |msg| {
488 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
489 let ml_dsa = MlDsa65::new();
490 ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
491 });
492
493 let content = [7u8; 32];
494 let quote = generator
495 .create_quote(content, 2048, 0)
496 .expect("create quote");
497
498 assert!(verify_quote_signature("e));
500
501 let mut tampered_quote = quote;
503 if let Some(byte) = tampered_quote.signature.first_mut() {
504 *byte ^= 0xFF;
505 }
506 assert!(!verify_quote_signature(&tampered_quote));
507 }
508
509 #[test]
510 fn test_empty_signature_fails_verification() {
511 let generator = create_test_generator();
512 let content = [42u8; 32];
513
514 let quote = generator
515 .create_quote(content, 1024, 0)
516 .expect("create quote");
517
518 assert!(!verify_quote_signature("e));
521 }
522
523 #[test]
524 fn test_rewards_address_getter() {
525 let addr = RewardsAddress::new([42u8; 20]);
526 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
527 let generator = QuoteGenerator::new(addr, metrics_tracker);
528
529 assert_eq!(*generator.rewards_address(), addr);
530 }
531
532 #[test]
533 fn test_current_metrics() {
534 let rewards_address = RewardsAddress::new([1u8; 20]);
535 let metrics_tracker = QuotingMetricsTracker::new(500, 50);
536 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
537
538 let metrics = generator.current_metrics();
539 assert_eq!(metrics.max_records, 500);
540 assert_eq!(metrics.close_records_stored, 50);
541 assert_eq!(metrics.data_size, 0);
542 assert_eq!(metrics.data_type, 0);
543 }
544
545 #[test]
546 fn test_record_payment_delegation() {
547 let rewards_address = RewardsAddress::new([1u8; 20]);
548 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
549 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
550
551 generator.record_payment();
552 generator.record_payment();
553
554 let metrics = generator.current_metrics();
555 assert_eq!(metrics.received_payment_count, 2);
556 }
557
558 #[test]
559 fn test_record_store_delegation() {
560 let rewards_address = RewardsAddress::new([1u8; 20]);
561 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
562 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
563
564 generator.record_store(0);
565 generator.record_store(1);
566 generator.record_store(0);
567
568 let metrics = generator.current_metrics();
569 assert_eq!(metrics.close_records_stored, 3);
570 }
571
572 #[test]
573 fn test_create_quote_different_data_types() {
574 let generator = create_test_generator();
575 let content = [10u8; 32];
576
577 let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
579 assert_eq!(q0.quoting_metrics.data_type, 0);
580
581 let q1 = generator.create_quote(content, 512, 1).expect("type 1");
583 assert_eq!(q1.quoting_metrics.data_type, 1);
584
585 let q2 = generator.create_quote(content, 256, 2).expect("type 2");
587 assert_eq!(q2.quoting_metrics.data_type, 2);
588 }
589
590 #[test]
591 fn test_create_quote_zero_size() {
592 let generator = create_test_generator();
593 let content = [11u8; 32];
594
595 let quote = generator.create_quote(content, 0, 0).expect("zero size");
596 assert_eq!(quote.quoting_metrics.data_size, 0);
597 }
598
599 #[test]
600 fn test_create_quote_large_size() {
601 let generator = create_test_generator();
602 let content = [12u8; 32];
603
604 let quote = generator
605 .create_quote(content, 10_000_000, 0)
606 .expect("large size");
607 assert_eq!(quote.quoting_metrics.data_size, 10_000_000);
608 }
609
610 #[test]
611 fn test_verify_quote_signature_empty_pub_key() {
612 let quote = PaymentQuote {
613 content: xor_name::XorName([0u8; 32]),
614 timestamp: SystemTime::now(),
615 quoting_metrics: ant_evm::QuotingMetrics {
616 data_size: 0,
617 data_type: 0,
618 close_records_stored: 0,
619 records_per_type: vec![],
620 max_records: 0,
621 received_payment_count: 0,
622 live_time: 0,
623 network_density: None,
624 network_size: None,
625 },
626 rewards_address: RewardsAddress::new([0u8; 20]),
627 pub_key: vec![],
628 signature: vec![],
629 };
630
631 assert!(!verify_quote_signature("e));
633 }
634
635 #[test]
636 fn test_can_sign_after_set_signer() {
637 let rewards_address = RewardsAddress::new([1u8; 20]);
638 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
639 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
640
641 assert!(!generator.can_sign());
642
643 generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
644
645 assert!(generator.can_sign());
646 }
647
648 #[test]
649 fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
650 let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
651 let rewards_address = RewardsAddress::new([3u8; 20]);
652 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
653 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
654
655 let result = wire_ml_dsa_signer(&mut generator, &identity);
656 assert!(
657 result.is_ok(),
658 "wire_ml_dsa_signer should succeed: {result:?}"
659 );
660 assert!(generator.can_sign());
661 }
662
663 #[test]
664 fn test_probe_signer_fails_without_signer() {
665 let rewards_address = RewardsAddress::new([1u8; 20]);
666 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
667 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
668
669 let result = generator.probe_signer();
670 assert!(result.is_err());
671 }
672
673 #[test]
674 fn test_probe_signer_fails_with_empty_signature() {
675 let rewards_address = RewardsAddress::new([1u8; 20]);
676 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
677 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
678
679 generator.set_signer(vec![0u8; 32], |_| vec![]);
680
681 let result = generator.probe_signer();
682 assert!(result.is_err());
683 }
684
685 #[test]
686 fn test_create_merkle_candidate_quote_with_ml_dsa() {
687 let ml_dsa = MlDsa65::new();
688 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
689
690 let rewards_address = RewardsAddress::new([0x42u8; 20]);
691 let metrics_tracker = QuotingMetricsTracker::new(800, 50);
692 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
693
694 let pub_key_bytes = public_key.as_bytes().to_vec();
696 let sk_bytes = secret_key.as_bytes().to_vec();
697 generator.set_signer(pub_key_bytes.clone(), move |msg| {
698 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
699 let ml_dsa = MlDsa65::new();
700 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
701 });
702
703 let timestamp = std::time::SystemTime::now()
704 .duration_since(std::time::UNIX_EPOCH)
705 .expect("system time")
706 .as_secs();
707
708 let result = generator.create_merkle_candidate_quote(2048, 0, timestamp);
709
710 assert!(
711 result.is_ok(),
712 "create_merkle_candidate_quote should succeed: {result:?}"
713 );
714
715 let candidate = result.expect("valid candidate");
716
717 assert_eq!(candidate.reward_address, rewards_address);
719
720 assert_eq!(candidate.merkle_payment_timestamp, timestamp);
722
723 assert_eq!(candidate.quoting_metrics.data_size, 2048);
725 assert_eq!(candidate.quoting_metrics.data_type, 0);
726 assert_eq!(candidate.quoting_metrics.max_records, 800);
727 assert_eq!(candidate.quoting_metrics.close_records_stored, 50);
728
729 assert_eq!(
731 candidate.pub_key, pub_key_bytes,
732 "Public key should be raw ML-DSA-65 bytes"
733 );
734
735 assert!(
737 verify_merkle_candidate_signature(&candidate),
738 "ML-DSA-65 merkle candidate signature must be valid"
739 );
740
741 let mut tampered = candidate;
743 tampered.merkle_payment_timestamp = timestamp + 1;
744 assert!(
745 !verify_merkle_candidate_signature(&tampered),
746 "Tampered timestamp should invalidate the ML-DSA-65 signature"
747 );
748 }
749}