use crate::EvmError;
use evmlib::{
common::{Address as RewardsAddress, QuoteHash},
quoting_metrics::QuotingMetrics,
};
use libp2p::{Multiaddr, PeerId, identity::PublicKey};
use serde::{Deserialize, Serialize};
pub use std::time::SystemTime;
use xor_name::XorName;
const LIVE_TIME_MARGIN: u64 = 10;
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct EncodedPeerId(Vec<u8>);
impl EncodedPeerId {
pub fn to_peer_id(&self) -> Result<PeerId, libp2p::identity::ParseError> {
PeerId::from_bytes(&self.0)
}
}
impl From<PeerId> for EncodedPeerId {
fn from(peer_id: PeerId) -> Self {
let bytes = peer_id.to_bytes();
EncodedPeerId(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct ClientProofOfPayment {
pub peer_quotes: Vec<(EncodedPeerId, Vec<Multiaddr>, PaymentQuote)>,
}
impl ClientProofOfPayment {
pub fn payees(&self) -> Vec<(PeerId, Vec<Multiaddr>)> {
self.peer_quotes
.iter()
.filter_map(|(peer_id, addrs, _)| {
if let Ok(peer_id) = peer_id.to_peer_id() {
Some((peer_id, addrs.clone()))
} else {
None
}
})
.collect()
}
pub fn to_proof_of_payment(&self) -> ProofOfPayment {
let peer_quotes = self
.peer_quotes
.iter()
.map(|(peer_id, _addrs, quote)| (peer_id.clone(), quote.clone()))
.collect();
ProofOfPayment { peer_quotes }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct ProofOfPayment {
pub peer_quotes: Vec<(EncodedPeerId, PaymentQuote)>,
}
impl ProofOfPayment {
pub fn digest(&self) -> Vec<(QuoteHash, QuotingMetrics, RewardsAddress)> {
self.peer_quotes
.clone()
.into_iter()
.map(|(_, quote)| (quote.hash(), quote.quoting_metrics, quote.rewards_address))
.collect()
}
pub fn payees(&self) -> Vec<PeerId> {
self.peer_quotes
.iter()
.filter_map(|(peer_id, _)| peer_id.to_peer_id().ok())
.collect()
}
pub fn quotes_by_peer(&self, peer_id: &PeerId) -> Vec<&PaymentQuote> {
self.peer_quotes
.iter()
.filter_map(|(_id, quote)| {
if let Ok(quote_peer_id) = quote.peer_id()
&& *peer_id == quote_peer_id
{
return Some(quote);
}
None
})
.collect()
}
pub fn verify_for(&self, peer_id: PeerId) -> bool {
if !self.payees().contains(&peer_id) {
warn!("Payment does not contain node peer id");
debug!("Payment contains peer ids: {:?}", self.payees());
debug!("Node peer id: {:?}", peer_id);
return false;
}
for (encoded_peer_id, quote) in self.peer_quotes.iter() {
let peer_id = match encoded_peer_id.to_peer_id() {
Ok(peer_id) => peer_id,
Err(e) => {
warn!("Invalid encoded peer id: {e}");
return false;
}
};
if !quote.check_is_signed_by_claimed_peer(peer_id) {
warn!("Payment is not signed by claimed peer");
return false;
}
}
true
}
pub fn verify_data_type(&self, data_type: u32) -> bool {
for (_, quote) in self.peer_quotes.iter() {
if quote.quoting_metrics.data_type != data_type {
return false;
}
}
true
}
}
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, custom_debug::Debug)]
pub struct PaymentQuote {
pub content: XorName,
pub timestamp: SystemTime,
pub quoting_metrics: QuotingMetrics,
pub rewards_address: RewardsAddress,
#[debug(skip)]
pub pub_key: Vec<u8>,
#[debug(skip)]
pub signature: Vec<u8>,
}
impl PaymentQuote {
pub fn hash(&self) -> QuoteHash {
let mut bytes = self.bytes_for_sig();
bytes.extend_from_slice(self.pub_key.as_slice());
bytes.extend_from_slice(self.signature.as_slice());
evmlib::cryptography::hash(bytes)
}
pub fn bytes_for_signing(
xorname: XorName,
timestamp: SystemTime,
quoting_metrics: &QuotingMetrics,
rewards_address: &RewardsAddress,
) -> Vec<u8> {
let mut bytes = xorname.to_vec();
bytes.extend_from_slice(
×tamp
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Unix epoch to be in the past")
.as_secs()
.to_le_bytes(),
);
let serialised_quoting_metrics = rmp_serde::to_vec(quoting_metrics).unwrap_or_default();
bytes.extend_from_slice(&serialised_quoting_metrics);
bytes.extend_from_slice(rewards_address.as_slice());
bytes
}
pub fn bytes_for_sig(&self) -> Vec<u8> {
Self::bytes_for_signing(
self.content,
self.timestamp,
&self.quoting_metrics,
&self.rewards_address,
)
}
pub fn peer_id(&self) -> Result<PeerId, EvmError> {
if let Ok(pub_key) = libp2p::identity::PublicKey::try_decode_protobuf(&self.pub_key) {
Ok(PeerId::from(pub_key.clone()))
} else {
error!("Can't parse PublicKey from protobuf");
Err(EvmError::InvalidQuotePublicKey)
}
}
pub fn check_is_signed_by_claimed_peer(&self, claimed_peer: PeerId) -> bool {
let pub_key = if let Ok(pub_key) = PublicKey::try_decode_protobuf(&self.pub_key) {
pub_key
} else {
error!("Can't parse PublicKey from protobuf");
return false;
};
let self_peer_id = PeerId::from(pub_key.clone());
if self_peer_id != claimed_peer {
error!("This quote {self:?} of {self_peer_id:?} is not signed by {claimed_peer:?}");
return false;
}
let bytes = self.bytes_for_sig();
if !pub_key.verify(&bytes, &self.signature) {
error!("Signature is not signed by claimed pub_key");
return false;
}
true
}
#[cfg(test)]
pub fn test_dummy(xorname: XorName) -> Self {
use evmlib::utils::dummy_address;
Self {
content: xorname,
timestamp: SystemTime::now(),
quoting_metrics: QuotingMetrics {
data_size: 0,
data_type: 0,
close_records_stored: 0,
records_per_type: vec![],
max_records: 0,
received_payment_count: 0,
live_time: 0,
network_density: None,
network_size: None,
},
pub_key: vec![],
signature: vec![],
rewards_address: dummy_address(),
}
}
pub fn is_newer_than(&self, other: &Self) -> bool {
self.timestamp > other.timestamp
}
pub fn historical_verify(&self, other: &Self) -> bool {
let self_is_newer = self.is_newer_than(other);
let (old_quote, new_quote) = if self_is_newer {
(other, self)
} else {
(self, other)
};
if new_quote.quoting_metrics.live_time < old_quote.quoting_metrics.live_time {
info!("Claimed live_time out of sequence");
return false;
}
if new_quote.quoting_metrics.received_payment_count
< old_quote.quoting_metrics.received_payment_count
{
info!("claimed received_payment_count out of sequence");
return false;
}
let old_elapsed = if let Ok(elapsed) = old_quote.timestamp.elapsed() {
elapsed
} else {
info!("old_quote timestamp elapsed call failure");
return true;
};
let new_elapsed = if let Ok(elapsed) = new_quote.timestamp.elapsed() {
elapsed
} else {
info!("new_quote timestamp elapsed call failure");
return true;
};
let time_diff = old_elapsed.as_secs().saturating_sub(new_elapsed.as_secs());
let live_time_diff =
new_quote.quoting_metrics.live_time - old_quote.quoting_metrics.live_time;
if live_time_diff > time_diff + LIVE_TIME_MARGIN {
info!("claimed live_time out of sync with the timestamp");
return false;
}
debug!(
"The new quote has {} close records stored, meanwhile old one has {}.",
new_quote.quoting_metrics.close_records_stored,
old_quote.quoting_metrics.close_records_stored
);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use libp2p::identity::Keypair;
use std::{thread::sleep, time::Duration};
#[test]
fn test_encode_decode_peer_id() {
let id = PeerId::random();
let encoded = EncodedPeerId::from(id);
let decoded = encoded.to_peer_id().expect("decode to work");
assert_eq!(id, decoded);
}
#[test]
fn test_is_newer_than() {
let old_quote = PaymentQuote::test_dummy(Default::default());
sleep(Duration::from_millis(100));
let new_quote = PaymentQuote::test_dummy(Default::default());
assert!(new_quote.is_newer_than(&old_quote));
assert!(!old_quote.is_newer_than(&new_quote));
}
#[test]
fn test_is_signed_by_claimed_peer() {
let keypair = Keypair::generate_ed25519();
let peer_id = keypair.public().to_peer_id();
let false_peer = PeerId::random();
let mut quote = PaymentQuote::test_dummy(Default::default());
let bytes = quote.bytes_for_sig();
let signature = if let Ok(sig) = keypair.sign(&bytes) {
sig
} else {
panic!("Cannot sign the quote!");
};
assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
quote.pub_key = keypair.public().encode_protobuf();
assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
quote.signature = signature;
assert!(quote.check_is_signed_by_claimed_peer(peer_id));
assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
quote.pub_key = Keypair::generate_ed25519().public().encode_protobuf();
assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
}
#[test]
fn test_historical_verify() {
let mut old_quote = PaymentQuote::test_dummy(Default::default());
sleep(Duration::from_millis(100));
let mut new_quote = PaymentQuote::test_dummy(Default::default());
assert!(new_quote.historical_verify(&old_quote));
assert!(old_quote.historical_verify(&new_quote));
old_quote.quoting_metrics.received_payment_count = 10;
new_quote.quoting_metrics.received_payment_count = 9;
assert!(!new_quote.historical_verify(&old_quote));
assert!(!old_quote.historical_verify(&new_quote));
new_quote.quoting_metrics.received_payment_count = 11;
assert!(new_quote.historical_verify(&old_quote));
assert!(old_quote.historical_verify(&new_quote));
new_quote.quoting_metrics.live_time = 10;
old_quote.quoting_metrics.live_time = 11;
assert!(!new_quote.historical_verify(&old_quote));
assert!(!old_quote.historical_verify(&new_quote));
new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN + 1;
assert!(!new_quote.historical_verify(&old_quote));
assert!(!old_quote.historical_verify(&new_quote));
new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN - 1;
assert!(new_quote.historical_verify(&old_quote));
assert!(old_quote.historical_verify(&new_quote));
}
}