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::MlDsaSecretKey;
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
248pub fn wire_ml_dsa_signer(
268 generator: &mut QuoteGenerator,
269 identity: &saorsa_core::identity::NodeIdentity,
270) -> Result<()> {
271 let pub_key_bytes = identity.public_key().as_bytes().to_vec();
272 let sk_bytes = identity.secret_key_bytes().to_vec();
273 let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
274 .map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
275 let ml_dsa = MlDsa65::new();
276 generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
277 Ok(sig) => sig.as_bytes().to_vec(),
278 Err(e) => {
279 crate::logging::error!("ML-DSA-65 signing failed: {e}");
280 vec![]
281 }
282 });
283 generator.probe_signer()?;
284 Ok(())
285}
286
287#[cfg(test)]
288#[allow(clippy::expect_used)]
289mod tests {
290 use super::*;
291 use crate::payment::metrics::QuotingMetricsTracker;
292 use ant_protocol::payment::verify::{
297 verify_merkle_candidate_signature, verify_quote_content, verify_quote_signature,
298 };
299 use evmlib::common::Amount;
300 use saorsa_pqc::pqc::types::MlDsaSecretKey;
301
302 fn create_test_generator() -> QuoteGenerator {
303 let rewards_address = RewardsAddress::new([1u8; 20]);
304 let metrics_tracker = QuotingMetricsTracker::new(100);
305
306 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
307
308 generator.set_signer(vec![0u8; 64], |bytes| {
310 let mut sig = vec![0u8; 64];
312 for (i, b) in bytes.iter().take(64).enumerate() {
313 sig[i] = *b;
314 }
315 sig
316 });
317
318 generator
319 }
320
321 #[test]
322 fn test_create_quote() {
323 let generator = create_test_generator();
324 let content = [42u8; 32];
325
326 let quote = generator.create_quote(content, 1024, 0);
327 assert!(quote.is_ok());
328
329 let quote = quote.expect("valid quote");
330 assert_eq!(quote.content.0, content);
331 }
332
333 #[test]
334 fn test_verify_quote_content() {
335 let generator = create_test_generator();
336 let content = [42u8; 32];
337
338 let quote = generator
339 .create_quote(content, 1024, 0)
340 .expect("valid quote");
341 assert!(verify_quote_content("e, &content));
342
343 let wrong_content = [99u8; 32];
345 assert!(!verify_quote_content("e, &wrong_content));
346 }
347
348 #[test]
349 fn test_generator_without_signer() {
350 let rewards_address = RewardsAddress::new([1u8; 20]);
351 let metrics_tracker = QuotingMetricsTracker::new(100);
352 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
353
354 assert!(!generator.can_sign());
355
356 let content = [42u8; 32];
357 let result = generator.create_quote(content, 1024, 0);
358 assert!(result.is_err());
359 }
360
361 #[test]
362 fn test_quote_signature_round_trip_real_keys() {
363 let ml_dsa = MlDsa65::new();
364 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
365
366 let rewards_address = RewardsAddress::new([2u8; 20]);
367 let metrics_tracker = QuotingMetricsTracker::new(100);
368 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
369
370 let pub_key_bytes = public_key.as_bytes().to_vec();
371 let sk_bytes = secret_key.as_bytes().to_vec();
372 generator.set_signer(pub_key_bytes, move |msg| {
373 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
374 let ml_dsa = MlDsa65::new();
375 ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
376 });
377
378 let content = [7u8; 32];
379 let quote = generator
380 .create_quote(content, 2048, 0)
381 .expect("create quote");
382
383 assert!(verify_quote_signature("e));
385
386 let mut tampered_quote = quote;
388 if let Some(byte) = tampered_quote.signature.first_mut() {
389 *byte ^= 0xFF;
390 }
391 assert!(!verify_quote_signature(&tampered_quote));
392 }
393
394 #[test]
395 fn test_empty_signature_fails_verification() {
396 let generator = create_test_generator();
397 let content = [42u8; 32];
398
399 let quote = generator
400 .create_quote(content, 1024, 0)
401 .expect("create quote");
402
403 assert!(!verify_quote_signature("e));
406 }
407
408 #[test]
409 fn test_rewards_address_getter() {
410 let addr = RewardsAddress::new([42u8; 20]);
411 let metrics_tracker = QuotingMetricsTracker::new(0);
412 let generator = QuoteGenerator::new(addr, metrics_tracker);
413
414 assert_eq!(*generator.rewards_address(), addr);
415 }
416
417 #[test]
418 fn test_records_stored() {
419 let rewards_address = RewardsAddress::new([1u8; 20]);
420 let metrics_tracker = QuotingMetricsTracker::new(50);
421 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
422
423 assert_eq!(generator.records_stored(), 50);
424 }
425
426 #[test]
427 fn test_record_store_delegation() {
428 let rewards_address = RewardsAddress::new([1u8; 20]);
429 let metrics_tracker = QuotingMetricsTracker::new(0);
430 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
431
432 generator.record_store(0);
433 generator.record_store(1);
434 generator.record_store(0);
435
436 assert_eq!(generator.records_stored(), 3);
437 }
438
439 #[test]
440 fn test_create_quote_different_data_types() {
441 let generator = create_test_generator();
442 let content = [10u8; 32];
443
444 let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
446 let q1 = generator.create_quote(content, 512, 1).expect("type 1");
447 let q2 = generator.create_quote(content, 256, 2).expect("type 2");
448
449 assert!(q0.price >= Amount::from(1u64));
451 assert!(q1.price >= Amount::from(1u64));
452 assert!(q2.price >= Amount::from(1u64));
453 }
454
455 #[test]
456 fn test_create_quote_zero_size() {
457 let generator = create_test_generator();
458 let content = [11u8; 32];
459
460 let quote = generator.create_quote(content, 0, 0).expect("zero size");
462 assert!(quote.price >= Amount::from(1u64));
463 }
464
465 #[test]
466 fn test_create_quote_large_size() {
467 let generator = create_test_generator();
468 let content = [12u8; 32];
469
470 let quote = generator
472 .create_quote(content, 10_000_000, 0)
473 .expect("large size");
474 assert!(quote.price >= Amount::from(1u64));
475 }
476
477 #[test]
478 fn test_verify_quote_signature_empty_pub_key() {
479 let quote = PaymentQuote {
480 content: xor_name::XorName([0u8; 32]),
481 timestamp: SystemTime::now(),
482 price: Amount::from(1u64),
483 rewards_address: RewardsAddress::new([0u8; 20]),
484 pub_key: vec![],
485 signature: vec![],
486 };
487
488 assert!(!verify_quote_signature("e));
490 }
491
492 #[test]
493 fn test_can_sign_after_set_signer() {
494 let rewards_address = RewardsAddress::new([1u8; 20]);
495 let metrics_tracker = QuotingMetricsTracker::new(0);
496 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
497
498 assert!(!generator.can_sign());
499
500 generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
501
502 assert!(generator.can_sign());
503 }
504
505 #[test]
506 fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
507 let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
508 let rewards_address = RewardsAddress::new([3u8; 20]);
509 let metrics_tracker = QuotingMetricsTracker::new(0);
510 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
511
512 let result = wire_ml_dsa_signer(&mut generator, &identity);
513 assert!(
514 result.is_ok(),
515 "wire_ml_dsa_signer should succeed: {result:?}"
516 );
517 assert!(generator.can_sign());
518 }
519
520 #[test]
521 fn test_probe_signer_fails_without_signer() {
522 let rewards_address = RewardsAddress::new([1u8; 20]);
523 let metrics_tracker = QuotingMetricsTracker::new(0);
524 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
525
526 let result = generator.probe_signer();
527 assert!(result.is_err());
528 }
529
530 #[test]
531 fn test_probe_signer_fails_with_empty_signature() {
532 let rewards_address = RewardsAddress::new([1u8; 20]);
533 let metrics_tracker = QuotingMetricsTracker::new(0);
534 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
535
536 generator.set_signer(vec![0u8; 32], |_| vec![]);
537
538 let result = generator.probe_signer();
539 assert!(result.is_err());
540 }
541
542 #[test]
543 fn test_create_merkle_candidate_quote_with_ml_dsa() {
544 let ml_dsa = MlDsa65::new();
545 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
546
547 let rewards_address = RewardsAddress::new([0x42u8; 20]);
548 let metrics_tracker = QuotingMetricsTracker::new(50);
549 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
550
551 let pub_key_bytes = public_key.as_bytes().to_vec();
553 let sk_bytes = secret_key.as_bytes().to_vec();
554 generator.set_signer(pub_key_bytes.clone(), move |msg| {
555 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
556 let ml_dsa = MlDsa65::new();
557 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
558 });
559
560 let timestamp = std::time::SystemTime::now()
561 .duration_since(std::time::UNIX_EPOCH)
562 .expect("system time")
563 .as_secs();
564
565 let result = generator.create_merkle_candidate_quote(2048, 0, timestamp);
566
567 assert!(
568 result.is_ok(),
569 "create_merkle_candidate_quote should succeed: {result:?}"
570 );
571
572 let candidate = result.expect("valid candidate");
573
574 assert_eq!(candidate.reward_address, rewards_address);
576
577 assert_eq!(candidate.merkle_payment_timestamp, timestamp);
579
580 assert_eq!(candidate.price, calculate_price(50));
582
583 assert_eq!(
585 candidate.pub_key, pub_key_bytes,
586 "Public key should be raw ML-DSA-65 bytes"
587 );
588
589 assert!(
591 verify_merkle_candidate_signature(&candidate),
592 "ML-DSA-65 merkle candidate signature must be valid"
593 );
594
595 let mut tampered = candidate;
597 tampered.merkle_payment_timestamp = timestamp + 1;
598 assert!(
599 !verify_merkle_candidate_signature(&tampered),
600 "Tampered timestamp should invalidate the ML-DSA-65 signature"
601 );
602 }
603
604 fn make_valid_merkle_candidate() -> MerklePaymentCandidateNode {
610 let ml_dsa = MlDsa65::new();
611 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
612
613 let rewards_address = RewardsAddress::new([0xABu8; 20]);
614 let timestamp = std::time::SystemTime::now()
615 .duration_since(std::time::UNIX_EPOCH)
616 .expect("system time")
617 .as_secs();
618 let price = Amount::from(42u64);
619
620 let msg = MerklePaymentCandidateNode::bytes_to_sign(&price, &rewards_address, timestamp);
621 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
622 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
623
624 MerklePaymentCandidateNode {
625 pub_key: public_key.as_bytes().to_vec(),
626 price,
627 reward_address: rewards_address,
628 merkle_payment_timestamp: timestamp,
629 signature,
630 }
631 }
632
633 #[test]
634 fn test_verify_merkle_candidate_valid_signature() {
635 let candidate = make_valid_merkle_candidate();
636 assert!(
637 verify_merkle_candidate_signature(&candidate),
638 "Freshly signed merkle candidate must verify"
639 );
640 }
641
642 #[test]
643 fn test_verify_merkle_candidate_tampered_pub_key() {
644 let mut candidate = make_valid_merkle_candidate();
645 if let Some(byte) = candidate.pub_key.first_mut() {
647 *byte ^= 0xFF;
648 }
649 assert!(
650 !verify_merkle_candidate_signature(&candidate),
651 "Tampered pub_key must invalidate the signature"
652 );
653 }
654
655 #[test]
656 fn test_verify_merkle_candidate_tampered_reward_address() {
657 let mut candidate = make_valid_merkle_candidate();
658 candidate.reward_address = RewardsAddress::new([0xFFu8; 20]);
659 assert!(
660 !verify_merkle_candidate_signature(&candidate),
661 "Tampered reward_address must invalidate the signature"
662 );
663 }
664
665 #[test]
666 fn test_verify_merkle_candidate_tampered_price() {
667 let mut candidate = make_valid_merkle_candidate();
668 candidate.price = Amount::from(999_999u64);
669 assert!(
670 !verify_merkle_candidate_signature(&candidate),
671 "Tampered price must invalidate the signature"
672 );
673 }
674
675 #[test]
676 fn test_verify_merkle_candidate_tampered_signature_byte() {
677 let mut candidate = make_valid_merkle_candidate();
678 if let Some(byte) = candidate.signature.first_mut() {
679 *byte ^= 0xFF;
680 }
681 assert!(
682 !verify_merkle_candidate_signature(&candidate),
683 "Tampered signature byte must fail verification"
684 );
685 }
686
687 #[test]
688 fn test_verify_merkle_candidate_empty_pub_key() {
689 let mut candidate = make_valid_merkle_candidate();
690 candidate.pub_key = vec![];
691 assert!(
692 !verify_merkle_candidate_signature(&candidate),
693 "Empty pub_key must fail verification"
694 );
695 }
696
697 #[test]
698 fn test_verify_merkle_candidate_empty_signature() {
699 let mut candidate = make_valid_merkle_candidate();
700 candidate.signature = vec![];
701 assert!(
702 !verify_merkle_candidate_signature(&candidate),
703 "Empty signature must fail verification"
704 );
705 }
706
707 #[test]
708 fn test_verify_merkle_candidate_wrong_length_signature() {
709 let mut candidate = make_valid_merkle_candidate();
710 candidate.signature = vec![0xAA; 100];
712 assert!(
713 !verify_merkle_candidate_signature(&candidate),
714 "Wrong-length signature must fail verification"
715 );
716 }
717
718 #[test]
719 fn test_verify_merkle_candidate_wrong_length_pub_key() {
720 let mut candidate = make_valid_merkle_candidate();
721 candidate.pub_key = vec![0xBB; 100];
723 assert!(
724 !verify_merkle_candidate_signature(&candidate),
725 "Wrong-length pub_key must fail verification"
726 );
727 }
728
729 #[test]
730 fn test_verify_merkle_candidate_cross_key_rejection() {
731 let candidate = make_valid_merkle_candidate();
733 let ml_dsa = MlDsa65::new();
734 let (other_pk, _) = ml_dsa.generate_keypair().expect("keygen");
735
736 let mut swapped = candidate;
737 swapped.pub_key = other_pk.as_bytes().to_vec();
738 assert!(
739 !verify_merkle_candidate_signature(&swapped),
740 "Signature from key A must not verify under key B"
741 );
742 }
743}