Skip to main content

saorsa_node/payment/
proof.rs

1//! Payment proof wrapper that includes transaction hashes.
2//!
3//! `PaymentProof` bundles a `ProofOfPayment` (quotes + peer IDs) with the
4//! on-chain transaction hashes returned by the wallet after payment.
5
6use ant_evm::ProofOfPayment;
7use evmlib::common::TxHash;
8use serde::{Deserialize, Serialize};
9
10/// A payment proof that includes both the quote-based proof and on-chain tx hashes.
11///
12/// This replaces the bare `ProofOfPayment` in serialized proof bytes, adding
13/// the transaction hashes that were previously discarded after `payment.pay()`.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PaymentProof {
16    /// The original quote-based proof (peer IDs + quotes with ML-DSA-65 signatures).
17    pub proof_of_payment: ProofOfPayment,
18    /// Transaction hashes from the on-chain payment.
19    /// Typically contains one hash for the median (non-zero) quote.
20    pub tx_hashes: Vec<TxHash>,
21}
22
23/// Deserialize proof bytes from the `PaymentProof` format.
24///
25/// Returns `(ProofOfPayment, Vec<TxHash>)`.
26///
27/// # Errors
28///
29/// Returns an error if the bytes cannot be deserialized.
30pub fn deserialize_proof(
31    bytes: &[u8],
32) -> std::result::Result<(ProofOfPayment, Vec<TxHash>), rmp_serde::decode::Error> {
33    let proof = rmp_serde::from_slice::<PaymentProof>(bytes)?;
34    Ok((proof.proof_of_payment, proof.tx_hashes))
35}
36
37#[cfg(test)]
38#[allow(clippy::unwrap_used, clippy::expect_used)]
39mod tests {
40    use super::*;
41    use alloy::primitives::FixedBytes;
42    use ant_evm::RewardsAddress;
43    use ant_evm::{EncodedPeerId, PaymentQuote};
44    use evmlib::quoting_metrics::QuotingMetrics;
45    use libp2p::identity::Keypair;
46    use libp2p::PeerId;
47    use std::time::SystemTime;
48    use xor_name::XorName;
49
50    fn make_test_quote() -> PaymentQuote {
51        PaymentQuote {
52            content: XorName::random(&mut rand::thread_rng()),
53            timestamp: SystemTime::now(),
54            quoting_metrics: QuotingMetrics {
55                data_size: 1024,
56                data_type: 0,
57                close_records_stored: 0,
58                records_per_type: vec![],
59                max_records: 1000,
60                received_payment_count: 0,
61                live_time: 0,
62                network_density: None,
63                network_size: None,
64            },
65            rewards_address: RewardsAddress::new([1u8; 20]),
66            pub_key: vec![],
67            signature: vec![],
68        }
69    }
70
71    fn make_proof_of_payment() -> ProofOfPayment {
72        let keypair = Keypair::generate_ed25519();
73        let peer_id = PeerId::from_public_key(&keypair.public());
74        ProofOfPayment {
75            peer_quotes: vec![(EncodedPeerId::from(peer_id), make_test_quote())],
76        }
77    }
78
79    #[test]
80    fn test_payment_proof_serialization_roundtrip() {
81        let tx_hash = FixedBytes::from([0xABu8; 32]);
82        let proof = PaymentProof {
83            proof_of_payment: make_proof_of_payment(),
84            tx_hashes: vec![tx_hash],
85        };
86
87        let bytes = rmp_serde::to_vec(&proof).unwrap();
88        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
89
90        assert_eq!(pop.peer_quotes.len(), 1);
91        assert_eq!(hashes.len(), 1);
92        assert_eq!(hashes.first().unwrap(), &tx_hash);
93    }
94
95    #[test]
96    fn test_payment_proof_with_empty_tx_hashes() {
97        let proof = PaymentProof {
98            proof_of_payment: make_proof_of_payment(),
99            tx_hashes: vec![],
100        };
101
102        let bytes = rmp_serde::to_vec(&proof).unwrap();
103        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
104
105        assert_eq!(pop.peer_quotes.len(), 1);
106        assert!(hashes.is_empty());
107    }
108
109    #[test]
110    fn test_deserialize_proof_rejects_garbage() {
111        let garbage = vec![0xFF, 0x00, 0x01, 0x02];
112        let result = deserialize_proof(&garbage);
113        assert!(result.is_err());
114    }
115
116    #[test]
117    fn test_payment_proof_multiple_tx_hashes() {
118        let tx1 = FixedBytes::from([0x11u8; 32]);
119        let tx2 = FixedBytes::from([0x22u8; 32]);
120        let proof = PaymentProof {
121            proof_of_payment: make_proof_of_payment(),
122            tx_hashes: vec![tx1, tx2],
123        };
124
125        let bytes = rmp_serde::to_vec(&proof).unwrap();
126        let (_, hashes) = deserialize_proof(&bytes).unwrap();
127
128        assert_eq!(hashes.len(), 2);
129        assert_eq!(hashes.first().unwrap(), &tx1);
130        assert_eq!(hashes.get(1).unwrap(), &tx2);
131    }
132}