ant_evm/
data_payments.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::EvmError;
10use evmlib::{
11    common::{Address as RewardsAddress, QuoteHash},
12    quoting_metrics::QuotingMetrics,
13};
14use libp2p::{identity::PublicKey, PeerId};
15use serde::{Deserialize, Serialize};
16pub use std::time::SystemTime;
17use xor_name::XorName;
18
19/// The time in seconds that a quote is valid for
20pub const QUOTE_EXPIRATION_SECS: u64 = 3600;
21
22/// The margin allowed for live_time
23const LIVE_TIME_MARGIN: u64 = 10;
24
25#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
26pub struct EncodedPeerId(Vec<u8>);
27
28impl EncodedPeerId {
29    pub fn to_peer_id(&self) -> Result<PeerId, libp2p::identity::ParseError> {
30        PeerId::from_bytes(&self.0)
31    }
32}
33
34impl From<PeerId> for EncodedPeerId {
35    fn from(peer_id: PeerId) -> Self {
36        let bytes = peer_id.to_bytes();
37        EncodedPeerId(bytes)
38    }
39}
40
41/// The proof of payment for a data payment
42#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
43pub struct ProofOfPayment {
44    pub peer_quotes: Vec<(EncodedPeerId, PaymentQuote)>,
45}
46
47impl ProofOfPayment {
48    /// returns a short digest of the proof of payment to use for verification
49    pub fn digest(&self) -> Vec<(QuoteHash, QuotingMetrics, RewardsAddress)> {
50        self.peer_quotes
51            .clone()
52            .into_iter()
53            .map(|(_, quote)| (quote.hash(), quote.quoting_metrics, quote.rewards_address))
54            .collect()
55    }
56
57    /// returns the list of payees
58    pub fn payees(&self) -> Vec<PeerId> {
59        self.peer_quotes
60            .iter()
61            .filter_map(|(peer_id, _)| peer_id.to_peer_id().ok())
62            .collect()
63    }
64
65    /// Returns all quotes by given peer id
66    pub fn quotes_by_peer(&self, peer_id: &PeerId) -> Vec<&PaymentQuote> {
67        self.peer_quotes
68            .iter()
69            .filter_map(|(_id, quote)| {
70                if let Ok(quote_peer_id) = quote.peer_id() {
71                    if *peer_id == quote_peer_id {
72                        return Some(quote);
73                    }
74                }
75                None
76            })
77            .collect()
78    }
79
80    /// verifies the proof of payment is valid for the given peer id
81    pub fn verify_for(&self, peer_id: PeerId) -> bool {
82        // make sure I am in the list of payees
83        if !self.payees().contains(&peer_id) {
84            warn!("Payment does not contain node peer id");
85            debug!("Payment contains peer ids: {:?}", self.payees());
86            debug!("Node peer id: {:?}", peer_id);
87            return false;
88        }
89
90        // verify all signatures
91        for (encoded_peer_id, quote) in self.peer_quotes.iter() {
92            let peer_id = match encoded_peer_id.to_peer_id() {
93                Ok(peer_id) => peer_id,
94                Err(e) => {
95                    warn!("Invalid encoded peer id: {e}");
96                    return false;
97                }
98            };
99            if !quote.check_is_signed_by_claimed_peer(peer_id) {
100                warn!("Payment is not signed by claimed peer");
101                return false;
102            }
103        }
104        true
105    }
106
107    /// Verifies whether all quotes were made for the expected data type.
108    pub fn verify_data_type(&self, data_type: u32) -> bool {
109        for (_, quote) in self.peer_quotes.iter() {
110            if quote.quoting_metrics.data_type != data_type {
111                return false;
112            }
113        }
114
115        true
116    }
117}
118
119/// A payment quote to store data given by a node to a client
120/// Note that the PaymentQuote is a contract between the node and itself to make sure the clients aren’t mispaying.
121/// It is NOT a contract between the client and the node.
122#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, custom_debug::Debug)]
123pub struct PaymentQuote {
124    /// the content paid for
125    pub content: XorName,
126    /// the local node time when the quote was created
127    pub timestamp: SystemTime,
128    /// quoting metrics being used to generate this quote
129    pub quoting_metrics: QuotingMetrics,
130    /// the node's wallet address
131    pub rewards_address: RewardsAddress,
132    /// the node's libp2p identity public key in bytes (PeerId)
133    #[debug(skip)]
134    pub pub_key: Vec<u8>,
135    /// the node's signature for the quote
136    #[debug(skip)]
137    pub signature: Vec<u8>,
138}
139
140impl PaymentQuote {
141    pub fn hash(&self) -> QuoteHash {
142        let mut bytes = self.bytes_for_sig();
143        bytes.extend_from_slice(self.pub_key.as_slice());
144        bytes.extend_from_slice(self.signature.as_slice());
145        evmlib::cryptography::hash(bytes)
146    }
147
148    /// returns the bytes to be signed from the given parameters
149    pub fn bytes_for_signing(
150        xorname: XorName,
151        timestamp: SystemTime,
152        quoting_metrics: &QuotingMetrics,
153        rewards_address: &RewardsAddress,
154    ) -> Vec<u8> {
155        let mut bytes = xorname.to_vec();
156        bytes.extend_from_slice(
157            &timestamp
158                .duration_since(SystemTime::UNIX_EPOCH)
159                .expect("Unix epoch to be in the past")
160                .as_secs()
161                .to_le_bytes(),
162        );
163        let serialised_quoting_metrics = rmp_serde::to_vec(quoting_metrics).unwrap_or_default();
164        bytes.extend_from_slice(&serialised_quoting_metrics);
165        bytes.extend_from_slice(rewards_address.as_slice());
166        bytes
167    }
168
169    /// Returns the bytes to be signed from self
170    pub fn bytes_for_sig(&self) -> Vec<u8> {
171        Self::bytes_for_signing(
172            self.content,
173            self.timestamp,
174            &self.quoting_metrics,
175            &self.rewards_address,
176        )
177    }
178
179    /// Returns the peer id of the node that created the quote
180    pub fn peer_id(&self) -> Result<PeerId, EvmError> {
181        if let Ok(pub_key) = libp2p::identity::PublicKey::try_decode_protobuf(&self.pub_key) {
182            Ok(PeerId::from(pub_key.clone()))
183        } else {
184            error!("Can't parse PublicKey from protobuf");
185            Err(EvmError::InvalidQuotePublicKey)
186        }
187    }
188
189    /// Check self is signed by the claimed peer
190    pub fn check_is_signed_by_claimed_peer(&self, claimed_peer: PeerId) -> bool {
191        let pub_key = if let Ok(pub_key) = PublicKey::try_decode_protobuf(&self.pub_key) {
192            pub_key
193        } else {
194            error!("Can't parse PublicKey from protobuf");
195            return false;
196        };
197
198        let self_peer_id = PeerId::from(pub_key.clone());
199
200        if self_peer_id != claimed_peer {
201            error!("This quote {self:?} of {self_peer_id:?} is not signed by {claimed_peer:?}");
202            return false;
203        }
204
205        let bytes = self.bytes_for_sig();
206
207        if !pub_key.verify(&bytes, &self.signature) {
208            error!("Signature is not signed by claimed pub_key");
209            return false;
210        }
211
212        true
213    }
214
215    /// Returns true if the quote has expired
216    pub fn has_expired(&self) -> bool {
217        let now = SystemTime::now();
218
219        let dur_s = match now.duration_since(self.timestamp) {
220            Ok(dur) => dur.as_secs(),
221            Err(err) => {
222                error!("Can't deduce elapsed time of {self:?} with error {err:?}");
223                return true;
224            }
225        };
226        dur_s > QUOTE_EXPIRATION_SECS
227    }
228
229    /// test utility to create a dummy quote
230    #[cfg(test)]
231    pub fn test_dummy(xorname: XorName) -> Self {
232        use evmlib::utils::dummy_address;
233
234        Self {
235            content: xorname,
236            timestamp: SystemTime::now(),
237            quoting_metrics: QuotingMetrics {
238                data_size: 0,
239                data_type: 0,
240                close_records_stored: 0,
241                records_per_type: vec![],
242                max_records: 0,
243                received_payment_count: 0,
244                live_time: 0,
245                network_density: None,
246                network_size: None,
247            },
248            pub_key: vec![],
249            signature: vec![],
250            rewards_address: dummy_address(),
251        }
252    }
253
254    /// Check whether self is newer than the target quote.
255    pub fn is_newer_than(&self, other: &Self) -> bool {
256        self.timestamp > other.timestamp
257    }
258
259    /// Check against a new quote, verify whether it is a valid one from self perspective.
260    /// Returns `true` to flag the `other` quote is valid, from self perspective.
261    pub fn historical_verify(&self, other: &Self) -> bool {
262        // There is a chance that an old quote got used later than a new quote
263        let self_is_newer = self.is_newer_than(other);
264        let (old_quote, new_quote) = if self_is_newer {
265            (other, self)
266        } else {
267            (self, other)
268        };
269
270        if new_quote.quoting_metrics.live_time < old_quote.quoting_metrics.live_time {
271            info!("Claimed live_time out of sequence");
272            return false;
273        }
274
275        // TODO: Double check if this applies, as this will prevent a node restart with same ID
276        if new_quote.quoting_metrics.received_payment_count
277            < old_quote.quoting_metrics.received_payment_count
278        {
279            info!("claimed received_payment_count out of sequence");
280            return false;
281        }
282
283        let old_elapsed = if let Ok(elapsed) = old_quote.timestamp.elapsed() {
284            elapsed
285        } else {
286            // The elapsed call could fail due to system clock change
287            // hence consider the verification succeeded.
288            info!("old_quote timestamp elapsed call failure");
289            return true;
290        };
291        let new_elapsed = if let Ok(elapsed) = new_quote.timestamp.elapsed() {
292            elapsed
293        } else {
294            // The elapsed call could fail due to system clock change
295            // hence consider the verification succeeded.
296            info!("new_quote timestamp elapsed call failure");
297            return true;
298        };
299
300        let time_diff = old_elapsed.as_secs().saturating_sub(new_elapsed.as_secs());
301        let live_time_diff =
302            new_quote.quoting_metrics.live_time - old_quote.quoting_metrics.live_time;
303        // In theory, these two shall match, give it a LIVE_TIME_MARGIN to avoid system glitch
304        if live_time_diff > time_diff + LIVE_TIME_MARGIN {
305            info!("claimed live_time out of sync with the timestamp");
306            return false;
307        }
308
309        // There could be pruning to be undertaken, also the close range keeps changing as well.
310        // Hence `close_records_stored` could be growing or shrinking.
311        // Currently not to carry out check on it, just logging to observe the trend.
312        debug!(
313            "The new quote has {} close records stored, meanwhile old one has {}.",
314            new_quote.quoting_metrics.close_records_stored,
315            old_quote.quoting_metrics.close_records_stored
316        );
317
318        true
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    use libp2p::identity::Keypair;
327    use std::{thread::sleep, time::Duration};
328
329    #[test]
330    fn test_encode_decode_peer_id() {
331        let id = PeerId::random();
332        let encoded = EncodedPeerId::from(id);
333        let decoded = encoded.to_peer_id().expect("decode to work");
334        assert_eq!(id, decoded);
335    }
336
337    #[test]
338    fn test_is_newer_than() {
339        let old_quote = PaymentQuote::test_dummy(Default::default());
340        sleep(Duration::from_millis(100));
341        let new_quote = PaymentQuote::test_dummy(Default::default());
342        assert!(new_quote.is_newer_than(&old_quote));
343        assert!(!old_quote.is_newer_than(&new_quote));
344    }
345
346    #[test]
347    fn test_is_signed_by_claimed_peer() {
348        let keypair = Keypair::generate_ed25519();
349        let peer_id = keypair.public().to_peer_id();
350
351        let false_peer = PeerId::random();
352
353        let mut quote = PaymentQuote::test_dummy(Default::default());
354        let bytes = quote.bytes_for_sig();
355        let signature = if let Ok(sig) = keypair.sign(&bytes) {
356            sig
357        } else {
358            panic!("Cannot sign the quote!");
359        };
360
361        // Check failed with both incorrect pub_key and signature
362        assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
363        assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
364
365        // Check failed with correct pub_key but incorrect signature
366        quote.pub_key = keypair.public().encode_protobuf();
367        assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
368        assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
369
370        // Check succeed with correct pub_key and signature,
371        // and failed with incorrect claimed signer (peer)
372        quote.signature = signature;
373        assert!(quote.check_is_signed_by_claimed_peer(peer_id));
374        assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
375
376        // Check failed with incorrect pub_key but correct signature
377        quote.pub_key = Keypair::generate_ed25519().public().encode_protobuf();
378        assert!(!quote.check_is_signed_by_claimed_peer(peer_id));
379        assert!(!quote.check_is_signed_by_claimed_peer(false_peer));
380    }
381
382    #[test]
383    fn test_historical_verify() {
384        let mut old_quote = PaymentQuote::test_dummy(Default::default());
385        sleep(Duration::from_millis(100));
386        let mut new_quote = PaymentQuote::test_dummy(Default::default());
387
388        // historical_verify will swap quotes to compare based on timeline automatically
389        assert!(new_quote.historical_verify(&old_quote));
390        assert!(old_quote.historical_verify(&new_quote));
391
392        // Out of sequence received_payment_count shall be detected
393        old_quote.quoting_metrics.received_payment_count = 10;
394        new_quote.quoting_metrics.received_payment_count = 9;
395        assert!(!new_quote.historical_verify(&old_quote));
396        assert!(!old_quote.historical_verify(&new_quote));
397        // Reset to correct one
398        new_quote.quoting_metrics.received_payment_count = 11;
399        assert!(new_quote.historical_verify(&old_quote));
400        assert!(old_quote.historical_verify(&new_quote));
401
402        // Out of sequence live_time shall be detected
403        new_quote.quoting_metrics.live_time = 10;
404        old_quote.quoting_metrics.live_time = 11;
405        assert!(!new_quote.historical_verify(&old_quote));
406        assert!(!old_quote.historical_verify(&new_quote));
407        // Out of margin live_time shall be detected
408        new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN + 1;
409        assert!(!new_quote.historical_verify(&old_quote));
410        assert!(!old_quote.historical_verify(&new_quote));
411        // Reset live_time to be within the margin
412        new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN - 1;
413        assert!(new_quote.historical_verify(&old_quote));
414        assert!(old_quote.historical_verify(&new_quote));
415    }
416}