use crate::error::{Error, Result};
use crate::logging::debug;
use crate::payment::metrics::QuotingMetricsTracker;
use crate::payment::pricing::calculate_price;
use evmlib::merkle_payments::MerklePaymentCandidateNode;
use evmlib::PaymentQuote;
use evmlib::RewardsAddress;
use saorsa_core::MlDsa65;
use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
use saorsa_pqc::pqc::MlDsaOperations;
use std::time::SystemTime;
pub type XorName = [u8; 32];
pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
pub struct QuoteGenerator {
rewards_address: RewardsAddress,
metrics_tracker: QuotingMetricsTracker,
sign_fn: Option<SignFn>,
pub_key: Vec<u8>,
}
impl QuoteGenerator {
#[must_use]
pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
Self {
rewards_address,
metrics_tracker,
sign_fn: None,
pub_key: Vec::new(),
}
}
pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
where
F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
{
self.pub_key = pub_key;
self.sign_fn = Some(Box::new(sign_fn));
}
#[must_use]
pub fn can_sign(&self) -> bool {
self.sign_fn.is_some()
}
pub fn probe_signer(&self) -> Result<()> {
let sign_fn = self
.sign_fn
.as_ref()
.ok_or_else(|| Error::Payment("Signer not set".to_string()))?;
let test_msg = b"ant-signing-probe";
let test_sig = sign_fn(test_msg);
if test_sig.is_empty() {
return Err(Error::Payment(
"ML-DSA-65 signing probe failed: empty signature produced".to_string(),
));
}
Ok(())
}
pub fn create_quote(
&self,
content: XorName,
data_size: usize,
data_type: u32,
) -> Result<PaymentQuote> {
let sign_fn = self
.sign_fn
.as_ref()
.ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
let timestamp = SystemTime::now();
let price = calculate_price(self.metrics_tracker.records_stored());
let xor_name = xor_name::XorName(content);
let bytes =
PaymentQuote::bytes_for_signing(xor_name, timestamp, &price, &self.rewards_address);
let signature = sign_fn(&bytes);
if signature.is_empty() {
return Err(Error::Payment(
"Signing produced empty signature".to_string(),
));
}
let quote = PaymentQuote {
content: xor_name,
timestamp,
price,
pub_key: self.pub_key.clone(),
rewards_address: self.rewards_address,
signature,
};
if crate::logging::enabled!(crate::logging::Level::DEBUG) {
let content_hex = hex::encode(content);
debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})");
}
Ok(quote)
}
#[must_use]
pub fn rewards_address(&self) -> &RewardsAddress {
&self.rewards_address
}
#[must_use]
pub fn records_stored(&self) -> usize {
self.metrics_tracker.records_stored()
}
pub fn record_payment(&self) {
self.metrics_tracker.record_payment();
}
pub fn record_store(&self, data_type: u32) {
self.metrics_tracker.record_store(data_type);
}
pub fn create_merkle_candidate_quote(
&self,
data_size: usize,
data_type: u32,
merkle_payment_timestamp: u64,
) -> Result<MerklePaymentCandidateNode> {
let sign_fn = self
.sign_fn
.as_ref()
.ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
let price = calculate_price(self.metrics_tracker.records_stored());
let msg = MerklePaymentCandidateNode::bytes_to_sign(
&price,
&self.rewards_address,
merkle_payment_timestamp,
);
let signature = sign_fn(&msg);
if signature.is_empty() {
return Err(Error::Payment(
"ML-DSA-65 signing produced empty signature for merkle candidate".to_string(),
));
}
let candidate = MerklePaymentCandidateNode {
pub_key: self.pub_key.clone(),
price,
reward_address: self.rewards_address,
merkle_payment_timestamp,
signature,
};
if crate::logging::enabled!(crate::logging::Level::DEBUG) {
debug!(
"Generated ML-DSA-65 merkle candidate quote (size: {data_size}, type: {data_type}, ts: {merkle_payment_timestamp})"
);
}
Ok(candidate)
}
}
#[must_use]
pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
if quote.content.0 != *expected_content {
if crate::logging::enabled!(crate::logging::Level::DEBUG) {
debug!(
"Quote content mismatch: expected {}, got {}",
hex::encode(expected_content),
hex::encode(quote.content.0)
);
}
return false;
}
true
}
#[must_use]
pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
Ok(pk) => pk,
Err(e) => {
debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
return false;
}
};
let signature = match MlDsaSignature::from_bytes("e.signature) {
Ok(sig) => sig,
Err(e) => {
debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
return false;
}
};
let bytes = quote.bytes_for_sig();
let ml_dsa = MlDsa65::new();
match ml_dsa.verify(&pub_key, &bytes, &signature) {
Ok(valid) => {
if !valid {
debug!("ML-DSA-65 quote signature verification failed");
}
valid
}
Err(e) => {
debug!("ML-DSA-65 verification error: {e}");
false
}
}
}
#[must_use]
pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
Ok(pk) => pk,
Err(e) => {
debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
return false;
}
};
let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
Ok(sig) => sig,
Err(e) => {
debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
return false;
}
};
let msg = MerklePaymentCandidateNode::bytes_to_sign(
&candidate.price,
&candidate.reward_address,
candidate.merkle_payment_timestamp,
);
let ml_dsa = MlDsa65::new();
match ml_dsa.verify(&pub_key, &msg, &signature) {
Ok(valid) => {
if !valid {
debug!("ML-DSA-65 merkle candidate signature verification failed");
}
valid
}
Err(e) => {
debug!("ML-DSA-65 merkle candidate verification error: {e}");
false
}
}
}
pub fn wire_ml_dsa_signer(
generator: &mut QuoteGenerator,
identity: &saorsa_core::identity::NodeIdentity,
) -> Result<()> {
let pub_key_bytes = identity.public_key().as_bytes().to_vec();
let sk_bytes = identity.secret_key_bytes().to_vec();
let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
.map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
let ml_dsa = MlDsa65::new();
generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
Ok(sig) => sig.as_bytes().to_vec(),
Err(e) => {
crate::logging::error!("ML-DSA-65 signing failed: {e}");
vec![]
}
});
generator.probe_signer()?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use crate::payment::metrics::QuotingMetricsTracker;
use evmlib::common::Amount;
use saorsa_pqc::pqc::types::MlDsaSecretKey;
fn create_test_generator() -> QuoteGenerator {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(100);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
generator.set_signer(vec![0u8; 64], |bytes| {
let mut sig = vec![0u8; 64];
for (i, b) in bytes.iter().take(64).enumerate() {
sig[i] = *b;
}
sig
});
generator
}
#[test]
fn test_create_quote() {
let generator = create_test_generator();
let content = [42u8; 32];
let quote = generator.create_quote(content, 1024, 0);
assert!(quote.is_ok());
let quote = quote.expect("valid quote");
assert_eq!(quote.content.0, content);
}
#[test]
fn test_verify_quote_content() {
let generator = create_test_generator();
let content = [42u8; 32];
let quote = generator
.create_quote(content, 1024, 0)
.expect("valid quote");
assert!(verify_quote_content("e, &content));
let wrong_content = [99u8; 32];
assert!(!verify_quote_content("e, &wrong_content));
}
#[test]
fn test_generator_without_signer() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(100);
let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
assert!(!generator.can_sign());
let content = [42u8; 32];
let result = generator.create_quote(content, 1024, 0);
assert!(result.is_err());
}
#[test]
fn test_quote_signature_round_trip_real_keys() {
let ml_dsa = MlDsa65::new();
let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
let rewards_address = RewardsAddress::new([2u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(100);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
let pub_key_bytes = public_key.as_bytes().to_vec();
let sk_bytes = secret_key.as_bytes().to_vec();
generator.set_signer(pub_key_bytes, move |msg| {
let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
let ml_dsa = MlDsa65::new();
ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
});
let content = [7u8; 32];
let quote = generator
.create_quote(content, 2048, 0)
.expect("create quote");
assert!(verify_quote_signature("e));
let mut tampered_quote = quote;
if let Some(byte) = tampered_quote.signature.first_mut() {
*byte ^= 0xFF;
}
assert!(!verify_quote_signature(&tampered_quote));
}
#[test]
fn test_empty_signature_fails_verification() {
let generator = create_test_generator();
let content = [42u8; 32];
let quote = generator
.create_quote(content, 1024, 0)
.expect("create quote");
assert!(!verify_quote_signature("e));
}
#[test]
fn test_rewards_address_getter() {
let addr = RewardsAddress::new([42u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let generator = QuoteGenerator::new(addr, metrics_tracker);
assert_eq!(*generator.rewards_address(), addr);
}
#[test]
fn test_records_stored() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(50);
let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
assert_eq!(generator.records_stored(), 50);
}
#[test]
fn test_record_store_delegation() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
generator.record_store(0);
generator.record_store(1);
generator.record_store(0);
assert_eq!(generator.records_stored(), 3);
}
#[test]
fn test_create_quote_different_data_types() {
let generator = create_test_generator();
let content = [10u8; 32];
let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
let q1 = generator.create_quote(content, 512, 1).expect("type 1");
let q2 = generator.create_quote(content, 256, 2).expect("type 2");
assert!(q0.price >= Amount::from(1u64));
assert!(q1.price >= Amount::from(1u64));
assert!(q2.price >= Amount::from(1u64));
}
#[test]
fn test_create_quote_zero_size() {
let generator = create_test_generator();
let content = [11u8; 32];
let quote = generator.create_quote(content, 0, 0).expect("zero size");
assert!(quote.price >= Amount::from(1u64));
}
#[test]
fn test_create_quote_large_size() {
let generator = create_test_generator();
let content = [12u8; 32];
let quote = generator
.create_quote(content, 10_000_000, 0)
.expect("large size");
assert!(quote.price >= Amount::from(1u64));
}
#[test]
fn test_verify_quote_signature_empty_pub_key() {
let quote = PaymentQuote {
content: xor_name::XorName([0u8; 32]),
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([0u8; 20]),
pub_key: vec![],
signature: vec![],
};
assert!(!verify_quote_signature("e));
}
#[test]
fn test_can_sign_after_set_signer() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
assert!(!generator.can_sign());
generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
assert!(generator.can_sign());
}
#[test]
fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
let rewards_address = RewardsAddress::new([3u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
let result = wire_ml_dsa_signer(&mut generator, &identity);
assert!(
result.is_ok(),
"wire_ml_dsa_signer should succeed: {result:?}"
);
assert!(generator.can_sign());
}
#[test]
fn test_probe_signer_fails_without_signer() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
let result = generator.probe_signer();
assert!(result.is_err());
}
#[test]
fn test_probe_signer_fails_with_empty_signature() {
let rewards_address = RewardsAddress::new([1u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(0);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
generator.set_signer(vec![0u8; 32], |_| vec![]);
let result = generator.probe_signer();
assert!(result.is_err());
}
#[test]
fn test_create_merkle_candidate_quote_with_ml_dsa() {
let ml_dsa = MlDsa65::new();
let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
let rewards_address = RewardsAddress::new([0x42u8; 20]);
let metrics_tracker = QuotingMetricsTracker::new(50);
let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
let pub_key_bytes = public_key.as_bytes().to_vec();
let sk_bytes = secret_key.as_bytes().to_vec();
generator.set_signer(pub_key_bytes.clone(), move |msg| {
let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
let ml_dsa = MlDsa65::new();
ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
});
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time")
.as_secs();
let result = generator.create_merkle_candidate_quote(2048, 0, timestamp);
assert!(
result.is_ok(),
"create_merkle_candidate_quote should succeed: {result:?}"
);
let candidate = result.expect("valid candidate");
assert_eq!(candidate.reward_address, rewards_address);
assert_eq!(candidate.merkle_payment_timestamp, timestamp);
assert_eq!(candidate.price, calculate_price(50));
assert_eq!(
candidate.pub_key, pub_key_bytes,
"Public key should be raw ML-DSA-65 bytes"
);
assert!(
verify_merkle_candidate_signature(&candidate),
"ML-DSA-65 merkle candidate signature must be valid"
);
let mut tampered = candidate;
tampered.merkle_payment_timestamp = timestamp + 1;
assert!(
!verify_merkle_candidate_signature(&tampered),
"Tampered timestamp should invalidate the ML-DSA-65 signature"
);
}
fn make_valid_merkle_candidate() -> MerklePaymentCandidateNode {
let ml_dsa = MlDsa65::new();
let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
let rewards_address = RewardsAddress::new([0xABu8; 20]);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time")
.as_secs();
let price = Amount::from(42u64);
let msg = MerklePaymentCandidateNode::bytes_to_sign(&price, &rewards_address, timestamp);
let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
MerklePaymentCandidateNode {
pub_key: public_key.as_bytes().to_vec(),
price,
reward_address: rewards_address,
merkle_payment_timestamp: timestamp,
signature,
}
}
#[test]
fn test_verify_merkle_candidate_valid_signature() {
let candidate = make_valid_merkle_candidate();
assert!(
verify_merkle_candidate_signature(&candidate),
"Freshly signed merkle candidate must verify"
);
}
#[test]
fn test_verify_merkle_candidate_tampered_pub_key() {
let mut candidate = make_valid_merkle_candidate();
if let Some(byte) = candidate.pub_key.first_mut() {
*byte ^= 0xFF;
}
assert!(
!verify_merkle_candidate_signature(&candidate),
"Tampered pub_key must invalidate the signature"
);
}
#[test]
fn test_verify_merkle_candidate_tampered_reward_address() {
let mut candidate = make_valid_merkle_candidate();
candidate.reward_address = RewardsAddress::new([0xFFu8; 20]);
assert!(
!verify_merkle_candidate_signature(&candidate),
"Tampered reward_address must invalidate the signature"
);
}
#[test]
fn test_verify_merkle_candidate_tampered_price() {
let mut candidate = make_valid_merkle_candidate();
candidate.price = Amount::from(999_999u64);
assert!(
!verify_merkle_candidate_signature(&candidate),
"Tampered price must invalidate the signature"
);
}
#[test]
fn test_verify_merkle_candidate_tampered_signature_byte() {
let mut candidate = make_valid_merkle_candidate();
if let Some(byte) = candidate.signature.first_mut() {
*byte ^= 0xFF;
}
assert!(
!verify_merkle_candidate_signature(&candidate),
"Tampered signature byte must fail verification"
);
}
#[test]
fn test_verify_merkle_candidate_empty_pub_key() {
let mut candidate = make_valid_merkle_candidate();
candidate.pub_key = vec![];
assert!(
!verify_merkle_candidate_signature(&candidate),
"Empty pub_key must fail verification"
);
}
#[test]
fn test_verify_merkle_candidate_empty_signature() {
let mut candidate = make_valid_merkle_candidate();
candidate.signature = vec![];
assert!(
!verify_merkle_candidate_signature(&candidate),
"Empty signature must fail verification"
);
}
#[test]
fn test_verify_merkle_candidate_wrong_length_signature() {
let mut candidate = make_valid_merkle_candidate();
candidate.signature = vec![0xAA; 100];
assert!(
!verify_merkle_candidate_signature(&candidate),
"Wrong-length signature must fail verification"
);
}
#[test]
fn test_verify_merkle_candidate_wrong_length_pub_key() {
let mut candidate = make_valid_merkle_candidate();
candidate.pub_key = vec![0xBB; 100];
assert!(
!verify_merkle_candidate_signature(&candidate),
"Wrong-length pub_key must fail verification"
);
}
#[test]
fn test_verify_merkle_candidate_cross_key_rejection() {
let candidate = make_valid_merkle_candidate();
let ml_dsa = MlDsa65::new();
let (other_pk, _) = ml_dsa.generate_keypair().expect("keygen");
let mut swapped = candidate;
swapped.pub_key = other_pk.as_bytes().to_vec();
assert!(
!verify_merkle_candidate_signature(&swapped),
"Signature from key A must not verify under key B"
);
}
}