1use crate::error::{Error, Result};
11use crate::logging::debug;
12use crate::payment::metrics::QuotingMetricsTracker;
13use crate::payment::pricing::calculate_price;
14use evmlib::merkle_payments::MerklePaymentCandidateNode;
15use evmlib::PaymentQuote;
16use evmlib::RewardsAddress;
17use saorsa_core::MlDsa65;
18use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
19use saorsa_pqc::pqc::MlDsaOperations;
20use std::time::SystemTime;
21
22pub type XorName = [u8; 32];
24
25pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
27
28pub struct QuoteGenerator {
33 rewards_address: RewardsAddress,
35 metrics_tracker: QuotingMetricsTracker,
37 sign_fn: Option<SignFn>,
40 pub_key: Vec<u8>,
42}
43
44impl QuoteGenerator {
45 #[must_use]
54 pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
55 Self {
56 rewards_address,
57 metrics_tracker,
58 sign_fn: None,
59 pub_key: Vec::new(),
60 }
61 }
62
63 pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
70 where
71 F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
72 {
73 self.pub_key = pub_key;
74 self.sign_fn = Some(Box::new(sign_fn));
75 }
76
77 #[must_use]
79 pub fn can_sign(&self) -> bool {
80 self.sign_fn.is_some()
81 }
82
83 pub fn probe_signer(&self) -> Result<()> {
89 let sign_fn = self
90 .sign_fn
91 .as_ref()
92 .ok_or_else(|| Error::Payment("Signer not set".to_string()))?;
93 let test_msg = b"ant-signing-probe";
94 let test_sig = sign_fn(test_msg);
95 if test_sig.is_empty() {
96 return Err(Error::Payment(
97 "ML-DSA-65 signing probe failed: empty signature produced".to_string(),
98 ));
99 }
100 Ok(())
101 }
102
103 pub fn create_quote(
119 &self,
120 content: XorName,
121 data_size: usize,
122 data_type: u32,
123 ) -> Result<PaymentQuote> {
124 let sign_fn = self
125 .sign_fn
126 .as_ref()
127 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
128
129 let timestamp = SystemTime::now();
130
131 let price = calculate_price(self.metrics_tracker.records_stored());
133
134 let xor_name = xor_name::XorName(content);
136
137 let bytes =
139 PaymentQuote::bytes_for_signing(xor_name, timestamp, &price, &self.rewards_address);
140
141 let signature = sign_fn(&bytes);
143 if signature.is_empty() {
144 return Err(Error::Payment(
145 "Signing produced empty signature".to_string(),
146 ));
147 }
148
149 let quote = PaymentQuote {
150 content: xor_name,
151 timestamp,
152 price,
153 pub_key: self.pub_key.clone(),
154 rewards_address: self.rewards_address,
155 signature,
156 };
157
158 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
159 let content_hex = hex::encode(content);
160 debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})");
161 }
162
163 Ok(quote)
164 }
165
166 #[must_use]
168 pub fn rewards_address(&self) -> &RewardsAddress {
169 &self.rewards_address
170 }
171
172 #[must_use]
174 pub fn records_stored(&self) -> usize {
175 self.metrics_tracker.records_stored()
176 }
177
178 pub fn record_payment(&self) {
180 self.metrics_tracker.record_payment();
181 }
182
183 pub fn record_store(&self, data_type: u32) {
185 self.metrics_tracker.record_store(data_type);
186 }
187
188 pub fn create_merkle_candidate_quote(
203 &self,
204 data_size: usize,
205 data_type: u32,
206 merkle_payment_timestamp: u64,
207 ) -> Result<MerklePaymentCandidateNode> {
208 let sign_fn = self
209 .sign_fn
210 .as_ref()
211 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
212
213 let price = calculate_price(self.metrics_tracker.records_stored());
214
215 let msg = MerklePaymentCandidateNode::bytes_to_sign(
217 &price,
218 &self.rewards_address,
219 merkle_payment_timestamp,
220 );
221
222 let signature = sign_fn(&msg);
224 if signature.is_empty() {
225 return Err(Error::Payment(
226 "ML-DSA-65 signing produced empty signature for merkle candidate".to_string(),
227 ));
228 }
229
230 let candidate = MerklePaymentCandidateNode {
231 pub_key: self.pub_key.clone(),
232 price,
233 reward_address: self.rewards_address,
234 merkle_payment_timestamp,
235 signature,
236 };
237
238 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
239 debug!(
240 "Generated ML-DSA-65 merkle candidate quote (size: {data_size}, type: {data_type}, ts: {merkle_payment_timestamp})"
241 );
242 }
243
244 Ok(candidate)
245 }
246}
247
248#[must_use]
259pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
260 if quote.content.0 != *expected_content {
262 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
263 debug!(
264 "Quote content mismatch: expected {}, got {}",
265 hex::encode(expected_content),
266 hex::encode(quote.content.0)
267 );
268 }
269 return false;
270 }
271 true
272}
273
274#[must_use]
288pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
289 let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
291 Ok(pk) => pk,
292 Err(e) => {
293 debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
294 return false;
295 }
296 };
297
298 let signature = match MlDsaSignature::from_bytes("e.signature) {
300 Ok(sig) => sig,
301 Err(e) => {
302 debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
303 return false;
304 }
305 };
306
307 let bytes = quote.bytes_for_sig();
309
310 let ml_dsa = MlDsa65::new();
312 match ml_dsa.verify(&pub_key, &bytes, &signature) {
313 Ok(valid) => {
314 if !valid {
315 debug!("ML-DSA-65 quote signature verification failed");
316 }
317 valid
318 }
319 Err(e) => {
320 debug!("ML-DSA-65 verification error: {e}");
321 false
322 }
323 }
324}
325
326#[must_use]
336pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
337 let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
338 Ok(pk) => pk,
339 Err(e) => {
340 debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
341 return false;
342 }
343 };
344
345 let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
346 Ok(sig) => sig,
347 Err(e) => {
348 debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
349 return false;
350 }
351 };
352
353 let msg = MerklePaymentCandidateNode::bytes_to_sign(
354 &candidate.price,
355 &candidate.reward_address,
356 candidate.merkle_payment_timestamp,
357 );
358
359 let ml_dsa = MlDsa65::new();
360 match ml_dsa.verify(&pub_key, &msg, &signature) {
361 Ok(valid) => {
362 if !valid {
363 debug!("ML-DSA-65 merkle candidate signature verification failed");
364 }
365 valid
366 }
367 Err(e) => {
368 debug!("ML-DSA-65 merkle candidate verification error: {e}");
369 false
370 }
371 }
372}
373
374pub fn wire_ml_dsa_signer(
389 generator: &mut QuoteGenerator,
390 identity: &saorsa_core::identity::NodeIdentity,
391) -> Result<()> {
392 let pub_key_bytes = identity.public_key().as_bytes().to_vec();
393 let sk_bytes = identity.secret_key_bytes().to_vec();
394 let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
395 .map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
396 let ml_dsa = MlDsa65::new();
397 generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
398 Ok(sig) => sig.as_bytes().to_vec(),
399 Err(e) => {
400 crate::logging::error!("ML-DSA-65 signing failed: {e}");
401 vec![]
402 }
403 });
404 generator.probe_signer()?;
405 Ok(())
406}
407
408#[cfg(test)]
409#[allow(clippy::expect_used)]
410mod tests {
411 use super::*;
412 use crate::payment::metrics::QuotingMetricsTracker;
413 use evmlib::common::Amount;
414 use saorsa_pqc::pqc::types::MlDsaSecretKey;
415
416 fn create_test_generator() -> QuoteGenerator {
417 let rewards_address = RewardsAddress::new([1u8; 20]);
418 let metrics_tracker = QuotingMetricsTracker::new(100);
419
420 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
421
422 generator.set_signer(vec![0u8; 64], |bytes| {
424 let mut sig = vec![0u8; 64];
426 for (i, b) in bytes.iter().take(64).enumerate() {
427 sig[i] = *b;
428 }
429 sig
430 });
431
432 generator
433 }
434
435 #[test]
436 fn test_create_quote() {
437 let generator = create_test_generator();
438 let content = [42u8; 32];
439
440 let quote = generator.create_quote(content, 1024, 0);
441 assert!(quote.is_ok());
442
443 let quote = quote.expect("valid quote");
444 assert_eq!(quote.content.0, content);
445 }
446
447 #[test]
448 fn test_verify_quote_content() {
449 let generator = create_test_generator();
450 let content = [42u8; 32];
451
452 let quote = generator
453 .create_quote(content, 1024, 0)
454 .expect("valid quote");
455 assert!(verify_quote_content("e, &content));
456
457 let wrong_content = [99u8; 32];
459 assert!(!verify_quote_content("e, &wrong_content));
460 }
461
462 #[test]
463 fn test_generator_without_signer() {
464 let rewards_address = RewardsAddress::new([1u8; 20]);
465 let metrics_tracker = QuotingMetricsTracker::new(100);
466 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
467
468 assert!(!generator.can_sign());
469
470 let content = [42u8; 32];
471 let result = generator.create_quote(content, 1024, 0);
472 assert!(result.is_err());
473 }
474
475 #[test]
476 fn test_quote_signature_round_trip_real_keys() {
477 let ml_dsa = MlDsa65::new();
478 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
479
480 let rewards_address = RewardsAddress::new([2u8; 20]);
481 let metrics_tracker = QuotingMetricsTracker::new(100);
482 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
483
484 let pub_key_bytes = public_key.as_bytes().to_vec();
485 let sk_bytes = secret_key.as_bytes().to_vec();
486 generator.set_signer(pub_key_bytes, move |msg| {
487 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
488 let ml_dsa = MlDsa65::new();
489 ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
490 });
491
492 let content = [7u8; 32];
493 let quote = generator
494 .create_quote(content, 2048, 0)
495 .expect("create quote");
496
497 assert!(verify_quote_signature("e));
499
500 let mut tampered_quote = quote;
502 if let Some(byte) = tampered_quote.signature.first_mut() {
503 *byte ^= 0xFF;
504 }
505 assert!(!verify_quote_signature(&tampered_quote));
506 }
507
508 #[test]
509 fn test_empty_signature_fails_verification() {
510 let generator = create_test_generator();
511 let content = [42u8; 32];
512
513 let quote = generator
514 .create_quote(content, 1024, 0)
515 .expect("create quote");
516
517 assert!(!verify_quote_signature("e));
520 }
521
522 #[test]
523 fn test_rewards_address_getter() {
524 let addr = RewardsAddress::new([42u8; 20]);
525 let metrics_tracker = QuotingMetricsTracker::new(0);
526 let generator = QuoteGenerator::new(addr, metrics_tracker);
527
528 assert_eq!(*generator.rewards_address(), addr);
529 }
530
531 #[test]
532 fn test_records_stored() {
533 let rewards_address = RewardsAddress::new([1u8; 20]);
534 let metrics_tracker = QuotingMetricsTracker::new(50);
535 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
536
537 assert_eq!(generator.records_stored(), 50);
538 }
539
540 #[test]
541 fn test_record_store_delegation() {
542 let rewards_address = RewardsAddress::new([1u8; 20]);
543 let metrics_tracker = QuotingMetricsTracker::new(0);
544 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
545
546 generator.record_store(0);
547 generator.record_store(1);
548 generator.record_store(0);
549
550 assert_eq!(generator.records_stored(), 3);
551 }
552
553 #[test]
554 fn test_create_quote_different_data_types() {
555 let generator = create_test_generator();
556 let content = [10u8; 32];
557
558 let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
560 let q1 = generator.create_quote(content, 512, 1).expect("type 1");
561 let q2 = generator.create_quote(content, 256, 2).expect("type 2");
562
563 assert!(q0.price >= Amount::from(1u64));
565 assert!(q1.price >= Amount::from(1u64));
566 assert!(q2.price >= Amount::from(1u64));
567 }
568
569 #[test]
570 fn test_create_quote_zero_size() {
571 let generator = create_test_generator();
572 let content = [11u8; 32];
573
574 let quote = generator.create_quote(content, 0, 0).expect("zero size");
576 assert!(quote.price >= Amount::from(1u64));
577 }
578
579 #[test]
580 fn test_create_quote_large_size() {
581 let generator = create_test_generator();
582 let content = [12u8; 32];
583
584 let quote = generator
586 .create_quote(content, 10_000_000, 0)
587 .expect("large size");
588 assert!(quote.price >= Amount::from(1u64));
589 }
590
591 #[test]
592 fn test_verify_quote_signature_empty_pub_key() {
593 let quote = PaymentQuote {
594 content: xor_name::XorName([0u8; 32]),
595 timestamp: SystemTime::now(),
596 price: Amount::from(1u64),
597 rewards_address: RewardsAddress::new([0u8; 20]),
598 pub_key: vec![],
599 signature: vec![],
600 };
601
602 assert!(!verify_quote_signature("e));
604 }
605
606 #[test]
607 fn test_can_sign_after_set_signer() {
608 let rewards_address = RewardsAddress::new([1u8; 20]);
609 let metrics_tracker = QuotingMetricsTracker::new(0);
610 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
611
612 assert!(!generator.can_sign());
613
614 generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
615
616 assert!(generator.can_sign());
617 }
618
619 #[test]
620 fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
621 let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
622 let rewards_address = RewardsAddress::new([3u8; 20]);
623 let metrics_tracker = QuotingMetricsTracker::new(0);
624 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
625
626 let result = wire_ml_dsa_signer(&mut generator, &identity);
627 assert!(
628 result.is_ok(),
629 "wire_ml_dsa_signer should succeed: {result:?}"
630 );
631 assert!(generator.can_sign());
632 }
633
634 #[test]
635 fn test_probe_signer_fails_without_signer() {
636 let rewards_address = RewardsAddress::new([1u8; 20]);
637 let metrics_tracker = QuotingMetricsTracker::new(0);
638 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
639
640 let result = generator.probe_signer();
641 assert!(result.is_err());
642 }
643
644 #[test]
645 fn test_probe_signer_fails_with_empty_signature() {
646 let rewards_address = RewardsAddress::new([1u8; 20]);
647 let metrics_tracker = QuotingMetricsTracker::new(0);
648 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
649
650 generator.set_signer(vec![0u8; 32], |_| vec![]);
651
652 let result = generator.probe_signer();
653 assert!(result.is_err());
654 }
655
656 #[test]
657 fn test_create_merkle_candidate_quote_with_ml_dsa() {
658 let ml_dsa = MlDsa65::new();
659 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
660
661 let rewards_address = RewardsAddress::new([0x42u8; 20]);
662 let metrics_tracker = QuotingMetricsTracker::new(50);
663 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
664
665 let pub_key_bytes = public_key.as_bytes().to_vec();
667 let sk_bytes = secret_key.as_bytes().to_vec();
668 generator.set_signer(pub_key_bytes.clone(), move |msg| {
669 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
670 let ml_dsa = MlDsa65::new();
671 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
672 });
673
674 let timestamp = std::time::SystemTime::now()
675 .duration_since(std::time::UNIX_EPOCH)
676 .expect("system time")
677 .as_secs();
678
679 let result = generator.create_merkle_candidate_quote(2048, 0, timestamp);
680
681 assert!(
682 result.is_ok(),
683 "create_merkle_candidate_quote should succeed: {result:?}"
684 );
685
686 let candidate = result.expect("valid candidate");
687
688 assert_eq!(candidate.reward_address, rewards_address);
690
691 assert_eq!(candidate.merkle_payment_timestamp, timestamp);
693
694 assert_eq!(candidate.price, calculate_price(50));
696
697 assert_eq!(
699 candidate.pub_key, pub_key_bytes,
700 "Public key should be raw ML-DSA-65 bytes"
701 );
702
703 assert!(
705 verify_merkle_candidate_signature(&candidate),
706 "ML-DSA-65 merkle candidate signature must be valid"
707 );
708
709 let mut tampered = candidate;
711 tampered.merkle_payment_timestamp = timestamp + 1;
712 assert!(
713 !verify_merkle_candidate_signature(&tampered),
714 "Tampered timestamp should invalidate the ML-DSA-65 signature"
715 );
716 }
717
718 fn make_valid_merkle_candidate() -> MerklePaymentCandidateNode {
724 let ml_dsa = MlDsa65::new();
725 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
726
727 let rewards_address = RewardsAddress::new([0xABu8; 20]);
728 let timestamp = std::time::SystemTime::now()
729 .duration_since(std::time::UNIX_EPOCH)
730 .expect("system time")
731 .as_secs();
732 let price = Amount::from(42u64);
733
734 let msg = MerklePaymentCandidateNode::bytes_to_sign(&price, &rewards_address, timestamp);
735 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
736 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
737
738 MerklePaymentCandidateNode {
739 pub_key: public_key.as_bytes().to_vec(),
740 price,
741 reward_address: rewards_address,
742 merkle_payment_timestamp: timestamp,
743 signature,
744 }
745 }
746
747 #[test]
748 fn test_verify_merkle_candidate_valid_signature() {
749 let candidate = make_valid_merkle_candidate();
750 assert!(
751 verify_merkle_candidate_signature(&candidate),
752 "Freshly signed merkle candidate must verify"
753 );
754 }
755
756 #[test]
757 fn test_verify_merkle_candidate_tampered_pub_key() {
758 let mut candidate = make_valid_merkle_candidate();
759 if let Some(byte) = candidate.pub_key.first_mut() {
761 *byte ^= 0xFF;
762 }
763 assert!(
764 !verify_merkle_candidate_signature(&candidate),
765 "Tampered pub_key must invalidate the signature"
766 );
767 }
768
769 #[test]
770 fn test_verify_merkle_candidate_tampered_reward_address() {
771 let mut candidate = make_valid_merkle_candidate();
772 candidate.reward_address = RewardsAddress::new([0xFFu8; 20]);
773 assert!(
774 !verify_merkle_candidate_signature(&candidate),
775 "Tampered reward_address must invalidate the signature"
776 );
777 }
778
779 #[test]
780 fn test_verify_merkle_candidate_tampered_price() {
781 let mut candidate = make_valid_merkle_candidate();
782 candidate.price = Amount::from(999_999u64);
783 assert!(
784 !verify_merkle_candidate_signature(&candidate),
785 "Tampered price must invalidate the signature"
786 );
787 }
788
789 #[test]
790 fn test_verify_merkle_candidate_tampered_signature_byte() {
791 let mut candidate = make_valid_merkle_candidate();
792 if let Some(byte) = candidate.signature.first_mut() {
793 *byte ^= 0xFF;
794 }
795 assert!(
796 !verify_merkle_candidate_signature(&candidate),
797 "Tampered signature byte must fail verification"
798 );
799 }
800
801 #[test]
802 fn test_verify_merkle_candidate_empty_pub_key() {
803 let mut candidate = make_valid_merkle_candidate();
804 candidate.pub_key = vec![];
805 assert!(
806 !verify_merkle_candidate_signature(&candidate),
807 "Empty pub_key must fail verification"
808 );
809 }
810
811 #[test]
812 fn test_verify_merkle_candidate_empty_signature() {
813 let mut candidate = make_valid_merkle_candidate();
814 candidate.signature = vec![];
815 assert!(
816 !verify_merkle_candidate_signature(&candidate),
817 "Empty signature must fail verification"
818 );
819 }
820
821 #[test]
822 fn test_verify_merkle_candidate_wrong_length_signature() {
823 let mut candidate = make_valid_merkle_candidate();
824 candidate.signature = vec![0xAA; 100];
826 assert!(
827 !verify_merkle_candidate_signature(&candidate),
828 "Wrong-length signature must fail verification"
829 );
830 }
831
832 #[test]
833 fn test_verify_merkle_candidate_wrong_length_pub_key() {
834 let mut candidate = make_valid_merkle_candidate();
835 candidate.pub_key = vec![0xBB; 100];
837 assert!(
838 !verify_merkle_candidate_signature(&candidate),
839 "Wrong-length pub_key must fail verification"
840 );
841 }
842
843 #[test]
844 fn test_verify_merkle_candidate_cross_key_rejection() {
845 let candidate = make_valid_merkle_candidate();
847 let ml_dsa = MlDsa65::new();
848 let (other_pk, _) = ml_dsa.generate_keypair().expect("keygen");
849
850 let mut swapped = candidate;
851 swapped.pub_key = other_pk.as_bytes().to_vec();
852 assert!(
853 !verify_merkle_candidate_signature(&swapped),
854 "Signature from key A must not verify under key B"
855 );
856 }
857}