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