Skip to main content

ant_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 crate::ant_protocol::{PROOF_TAG_MERKLE, PROOF_TAG_SINGLE_NODE};
7use ant_evm::merkle_payments::MerklePaymentProof;
8use ant_evm::ProofOfPayment;
9use evmlib::common::TxHash;
10use serde::{Deserialize, Serialize};
11
12/// A payment proof that includes both the quote-based proof and on-chain tx hashes.
13///
14/// This replaces the bare `ProofOfPayment` in serialized proof bytes, adding
15/// the transaction hashes that were previously discarded after `payment.pay()`.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PaymentProof {
18    /// The original quote-based proof (peer IDs + quotes with ML-DSA-65 signatures).
19    pub proof_of_payment: ProofOfPayment,
20    /// Transaction hashes from the on-chain payment.
21    /// Typically contains one hash for the median (non-zero) quote.
22    pub tx_hashes: Vec<TxHash>,
23}
24
25/// The detected type of a payment proof.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ProofType {
28    /// `SingleNode` payment (5 quotes, median-paid).
29    SingleNode,
30    /// Merkle batch payment (one tx for many chunks).
31    Merkle,
32}
33
34/// Detect the proof type from the first byte (version tag).
35///
36/// Returns `None` if the tag byte is unrecognized or the slice is empty.
37#[must_use]
38pub fn detect_proof_type(bytes: &[u8]) -> Option<ProofType> {
39    match bytes.first() {
40        Some(&PROOF_TAG_SINGLE_NODE) => Some(ProofType::SingleNode),
41        Some(&PROOF_TAG_MERKLE) => Some(ProofType::Merkle),
42        _ => None,
43    }
44}
45
46/// Serialize a `PaymentProof` (single-node) with the version tag prefix.
47///
48/// # Errors
49///
50/// Returns an error if serialization fails.
51pub fn serialize_single_node_proof(
52    proof: &PaymentProof,
53) -> std::result::Result<Vec<u8>, rmp_serde::encode::Error> {
54    let body = rmp_serde::to_vec(proof)?;
55    let mut tagged = Vec::with_capacity(1 + body.len());
56    tagged.push(PROOF_TAG_SINGLE_NODE);
57    tagged.extend_from_slice(&body);
58    Ok(tagged)
59}
60
61/// Serialize a `MerklePaymentProof` with the version tag prefix.
62///
63/// # Errors
64///
65/// Returns an error if serialization fails.
66pub fn serialize_merkle_proof(
67    proof: &MerklePaymentProof,
68) -> std::result::Result<Vec<u8>, rmp_serde::encode::Error> {
69    let body = rmp_serde::to_vec(proof)?;
70    let mut tagged = Vec::with_capacity(1 + body.len());
71    tagged.push(PROOF_TAG_MERKLE);
72    tagged.extend_from_slice(&body);
73    Ok(tagged)
74}
75
76/// Deserialize proof bytes from the `PaymentProof` format (single-node).
77///
78/// Expects the first byte to be `PROOF_TAG_SINGLE_NODE`.
79/// Returns `(ProofOfPayment, Vec<TxHash>)`.
80///
81/// # Errors
82///
83/// Returns an error if the tag is missing or the bytes cannot be deserialized.
84pub fn deserialize_proof(bytes: &[u8]) -> Result<(ProofOfPayment, Vec<TxHash>), String> {
85    if bytes.first() != Some(&PROOF_TAG_SINGLE_NODE) {
86        return Err("Missing single-node proof tag byte".to_string());
87    }
88    let payload = bytes
89        .get(1..)
90        .ok_or_else(|| "Single-node proof tag present but no payload".to_string())?;
91    let proof = rmp_serde::from_slice::<PaymentProof>(payload)
92        .map_err(|e| format!("Failed to deserialize single-node proof: {e}"))?;
93    Ok((proof.proof_of_payment, proof.tx_hashes))
94}
95
96/// Deserialize proof bytes as a `MerklePaymentProof`.
97///
98/// Expects the first byte to be `PROOF_TAG_MERKLE`.
99///
100/// # Errors
101///
102/// Returns an error if the bytes cannot be deserialized or the tag is wrong.
103pub fn deserialize_merkle_proof(bytes: &[u8]) -> std::result::Result<MerklePaymentProof, String> {
104    if bytes.first() != Some(&PROOF_TAG_MERKLE) {
105        return Err("Missing merkle proof tag byte".to_string());
106    }
107    let payload = bytes
108        .get(1..)
109        .ok_or_else(|| "Merkle proof tag present but no payload".to_string())?;
110    rmp_serde::from_slice::<MerklePaymentProof>(payload)
111        .map_err(|e| format!("Failed to deserialize merkle proof: {e}"))
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
116mod tests {
117    use super::*;
118    use alloy::primitives::FixedBytes;
119    use ant_evm::merkle_payments::{
120        MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree,
121        CANDIDATES_PER_POOL,
122    };
123    use ant_evm::RewardsAddress;
124    use ant_evm::{EncodedPeerId, PaymentQuote};
125    use evmlib::quoting_metrics::QuotingMetrics;
126    use libp2p::identity::Keypair;
127    use libp2p::PeerId;
128    use saorsa_core::MlDsa65;
129    use saorsa_pqc::pqc::types::MlDsaSecretKey;
130    use saorsa_pqc::pqc::MlDsaOperations;
131    use std::time::SystemTime;
132    use xor_name::XorName;
133
134    fn make_test_quote() -> PaymentQuote {
135        PaymentQuote {
136            content: XorName::random(&mut rand::thread_rng()),
137            timestamp: SystemTime::now(),
138            quoting_metrics: QuotingMetrics {
139                data_size: 1024,
140                data_type: 0,
141                close_records_stored: 0,
142                records_per_type: vec![],
143                max_records: 1000,
144                received_payment_count: 0,
145                live_time: 0,
146                network_density: None,
147                network_size: None,
148            },
149            rewards_address: RewardsAddress::new([1u8; 20]),
150            pub_key: vec![],
151            signature: vec![],
152        }
153    }
154
155    fn make_proof_of_payment() -> ProofOfPayment {
156        let keypair = Keypair::generate_ed25519();
157        let peer_id = PeerId::from_public_key(&keypair.public());
158        ProofOfPayment {
159            peer_quotes: vec![(EncodedPeerId::from(peer_id), make_test_quote())],
160        }
161    }
162
163    #[test]
164    fn test_payment_proof_serialization_roundtrip() {
165        let tx_hash = FixedBytes::from([0xABu8; 32]);
166        let proof = PaymentProof {
167            proof_of_payment: make_proof_of_payment(),
168            tx_hashes: vec![tx_hash],
169        };
170
171        let bytes = serialize_single_node_proof(&proof).unwrap();
172        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
173
174        assert_eq!(pop.peer_quotes.len(), 1);
175        assert_eq!(hashes.len(), 1);
176        assert_eq!(hashes.first().unwrap(), &tx_hash);
177    }
178
179    #[test]
180    fn test_payment_proof_with_empty_tx_hashes() {
181        let proof = PaymentProof {
182            proof_of_payment: make_proof_of_payment(),
183            tx_hashes: vec![],
184        };
185
186        let bytes = serialize_single_node_proof(&proof).unwrap();
187        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
188
189        assert_eq!(pop.peer_quotes.len(), 1);
190        assert!(hashes.is_empty());
191    }
192
193    #[test]
194    fn test_deserialize_proof_rejects_garbage() {
195        let garbage = vec![0xFF, 0x00, 0x01, 0x02];
196        let result = deserialize_proof(&garbage);
197        assert!(result.is_err());
198    }
199
200    #[test]
201    fn test_deserialize_proof_rejects_untagged() {
202        // Raw msgpack without tag byte must be rejected
203        let proof = PaymentProof {
204            proof_of_payment: make_proof_of_payment(),
205            tx_hashes: vec![],
206        };
207        let raw_bytes = rmp_serde::to_vec(&proof).unwrap();
208        let result = deserialize_proof(&raw_bytes);
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_payment_proof_multiple_tx_hashes() {
214        let tx1 = FixedBytes::from([0x11u8; 32]);
215        let tx2 = FixedBytes::from([0x22u8; 32]);
216        let proof = PaymentProof {
217            proof_of_payment: make_proof_of_payment(),
218            tx_hashes: vec![tx1, tx2],
219        };
220
221        let bytes = serialize_single_node_proof(&proof).unwrap();
222        let (_, hashes) = deserialize_proof(&bytes).unwrap();
223
224        assert_eq!(hashes.len(), 2);
225        assert_eq!(hashes.first().unwrap(), &tx1);
226        assert_eq!(hashes.get(1).unwrap(), &tx2);
227    }
228
229    // =========================================================================
230    // detect_proof_type tests
231    // =========================================================================
232
233    #[test]
234    fn test_detect_proof_type_single_node() {
235        let bytes = [PROOF_TAG_SINGLE_NODE, 0x00, 0x01];
236        let result = detect_proof_type(&bytes);
237        assert_eq!(result, Some(ProofType::SingleNode));
238    }
239
240    #[test]
241    fn test_detect_proof_type_merkle() {
242        let bytes = [PROOF_TAG_MERKLE, 0x00, 0x01];
243        let result = detect_proof_type(&bytes);
244        assert_eq!(result, Some(ProofType::Merkle));
245    }
246
247    #[test]
248    fn test_detect_proof_type_unknown_tag() {
249        let bytes = [0xFF, 0x00, 0x01];
250        let result = detect_proof_type(&bytes);
251        assert_eq!(result, None);
252    }
253
254    #[test]
255    fn test_detect_proof_type_empty_bytes() {
256        let bytes: &[u8] = &[];
257        let result = detect_proof_type(bytes);
258        assert_eq!(result, None);
259    }
260
261    // =========================================================================
262    // Tagged serialize/deserialize round-trip tests
263    // =========================================================================
264
265    #[test]
266    fn test_serialize_single_node_proof_roundtrip_with_tag() {
267        let tx_hash = FixedBytes::from([0xCCu8; 32]);
268        let proof = PaymentProof {
269            proof_of_payment: make_proof_of_payment(),
270            tx_hashes: vec![tx_hash],
271        };
272
273        let tagged_bytes = serialize_single_node_proof(&proof).unwrap();
274
275        // First byte must be the single-node tag
276        assert_eq!(
277            tagged_bytes.first().copied(),
278            Some(PROOF_TAG_SINGLE_NODE),
279            "Tagged proof must start with PROOF_TAG_SINGLE_NODE"
280        );
281
282        // detect_proof_type should identify it
283        assert_eq!(
284            detect_proof_type(&tagged_bytes),
285            Some(ProofType::SingleNode)
286        );
287
288        // deserialize_proof handles the tag transparently
289        let (pop, hashes) = deserialize_proof(&tagged_bytes).unwrap();
290        assert_eq!(pop.peer_quotes.len(), 1);
291        assert_eq!(hashes.len(), 1);
292        assert_eq!(hashes.first().unwrap(), &tx_hash);
293    }
294
295    // =========================================================================
296    // Merkle proof serialize/deserialize round-trip tests
297    // =========================================================================
298
299    /// Create a minimal valid `MerklePaymentProof` from a small merkle tree.
300    fn make_test_merkle_proof() -> MerklePaymentProof {
301        let timestamp = std::time::SystemTime::now()
302            .duration_since(std::time::UNIX_EPOCH)
303            .unwrap()
304            .as_secs();
305
306        // Build a tree with 4 addresses (minimal depth)
307        let addresses: Vec<xor_name::XorName> = (0..4u8)
308            .map(|i| xor_name::XorName::from_content(&[i]))
309            .collect();
310        let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
311
312        // Build candidate nodes with ML-DSA-65 signing (matching production)
313        let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
314            std::array::from_fn(|i| {
315                let ml_dsa = MlDsa65::new();
316                let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
317                let metrics = QuotingMetrics {
318                    data_size: 1024,
319                    data_type: 0,
320                    close_records_stored: i * 10,
321                    records_per_type: vec![],
322                    max_records: 500,
323                    received_payment_count: 0,
324                    live_time: 100,
325                    network_density: None,
326                    network_size: None,
327                };
328                #[allow(clippy::cast_possible_truncation)]
329                let reward_address = RewardsAddress::new([i as u8; 20]);
330                let msg =
331                    MerklePaymentCandidateNode::bytes_to_sign(&metrics, &reward_address, timestamp);
332                let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
333                let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
334
335                MerklePaymentCandidateNode {
336                    pub_key: pub_key.as_bytes().to_vec(),
337                    quoting_metrics: metrics,
338                    reward_address,
339                    merkle_payment_timestamp: timestamp,
340                    signature,
341                }
342            });
343
344        let reward_candidates = tree.reward_candidates(timestamp).unwrap();
345        let midpoint_proof = reward_candidates.first().unwrap().clone();
346
347        let pool = MerklePaymentCandidatePool {
348            midpoint_proof,
349            candidate_nodes,
350        };
351
352        let first_address = *addresses.first().unwrap();
353        let address_proof = tree.generate_address_proof(0, first_address).unwrap();
354
355        MerklePaymentProof::new(first_address, address_proof, pool)
356    }
357
358    #[test]
359    fn test_serialize_merkle_proof_roundtrip() {
360        let merkle_proof = make_test_merkle_proof();
361
362        let tagged_bytes = serialize_merkle_proof(&merkle_proof).unwrap();
363
364        // First byte must be the merkle tag
365        assert_eq!(
366            tagged_bytes.first().copied(),
367            Some(PROOF_TAG_MERKLE),
368            "Tagged merkle proof must start with PROOF_TAG_MERKLE"
369        );
370
371        // detect_proof_type should identify it as merkle
372        assert_eq!(detect_proof_type(&tagged_bytes), Some(ProofType::Merkle));
373
374        // deserialize_merkle_proof should recover the original proof
375        let recovered = deserialize_merkle_proof(&tagged_bytes).unwrap();
376        assert_eq!(recovered.address, merkle_proof.address);
377        assert_eq!(
378            recovered.winner_pool.candidate_nodes.len(),
379            CANDIDATES_PER_POOL
380        );
381    }
382
383    #[test]
384    fn test_deserialize_merkle_proof_rejects_wrong_tag() {
385        let merkle_proof = make_test_merkle_proof();
386        let mut tagged_bytes = serialize_merkle_proof(&merkle_proof).unwrap();
387
388        // Replace the tag with the single-node tag
389        if let Some(first) = tagged_bytes.first_mut() {
390            *first = PROOF_TAG_SINGLE_NODE;
391        }
392
393        let result = deserialize_merkle_proof(&tagged_bytes);
394        assert!(result.is_err(), "Should reject wrong tag byte");
395        let err_msg = result.unwrap_err();
396        assert!(
397            err_msg.contains("Missing merkle proof tag"),
398            "Error should mention missing tag: {err_msg}"
399        );
400    }
401
402    #[test]
403    fn test_deserialize_merkle_proof_rejects_empty() {
404        let result = deserialize_merkle_proof(&[]);
405        assert!(result.is_err());
406    }
407}