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 evmlib::common::TxHash;
8use evmlib::merkle_payments::MerklePaymentProof;
9use evmlib::ProofOfPayment;
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 (`CLOSE_GROUP_SIZE` 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 evmlib::common::Amount;
120    use evmlib::merkle_payments::{
121        MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof, MerkleTree,
122        CANDIDATES_PER_POOL,
123    };
124    use evmlib::EncodedPeerId;
125    use evmlib::PaymentQuote;
126    use evmlib::RewardsAddress;
127    use saorsa_core::MlDsa65;
128    use saorsa_pqc::pqc::types::MlDsaSecretKey;
129    use saorsa_pqc::pqc::MlDsaOperations;
130    use std::time::SystemTime;
131    use xor_name::XorName;
132
133    fn make_test_quote() -> PaymentQuote {
134        PaymentQuote {
135            content: XorName::random(&mut rand::thread_rng()),
136            timestamp: SystemTime::now(),
137            price: Amount::from(1u64),
138            rewards_address: RewardsAddress::new([1u8; 20]),
139            pub_key: vec![],
140            signature: vec![],
141        }
142    }
143
144    fn make_proof_of_payment() -> ProofOfPayment {
145        let random_peer = EncodedPeerId::new(rand::random());
146        ProofOfPayment {
147            peer_quotes: vec![(random_peer, make_test_quote())],
148        }
149    }
150
151    #[test]
152    fn test_payment_proof_serialization_roundtrip() {
153        let tx_hash = FixedBytes::from([0xABu8; 32]);
154        let proof = PaymentProof {
155            proof_of_payment: make_proof_of_payment(),
156            tx_hashes: vec![tx_hash],
157        };
158
159        let bytes = serialize_single_node_proof(&proof).unwrap();
160        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
161
162        assert_eq!(pop.peer_quotes.len(), 1);
163        assert_eq!(hashes.len(), 1);
164        assert_eq!(hashes.first().unwrap(), &tx_hash);
165    }
166
167    #[test]
168    fn test_payment_proof_with_empty_tx_hashes() {
169        let proof = PaymentProof {
170            proof_of_payment: make_proof_of_payment(),
171            tx_hashes: vec![],
172        };
173
174        let bytes = serialize_single_node_proof(&proof).unwrap();
175        let (pop, hashes) = deserialize_proof(&bytes).unwrap();
176
177        assert_eq!(pop.peer_quotes.len(), 1);
178        assert!(hashes.is_empty());
179    }
180
181    #[test]
182    fn test_deserialize_proof_rejects_garbage() {
183        let garbage = vec![0xFF, 0x00, 0x01, 0x02];
184        let result = deserialize_proof(&garbage);
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_deserialize_proof_rejects_untagged() {
190        // Raw msgpack without tag byte must be rejected
191        let proof = PaymentProof {
192            proof_of_payment: make_proof_of_payment(),
193            tx_hashes: vec![],
194        };
195        let raw_bytes = rmp_serde::to_vec(&proof).unwrap();
196        let result = deserialize_proof(&raw_bytes);
197        assert!(result.is_err());
198    }
199
200    #[test]
201    fn test_payment_proof_multiple_tx_hashes() {
202        let tx1 = FixedBytes::from([0x11u8; 32]);
203        let tx2 = FixedBytes::from([0x22u8; 32]);
204        let proof = PaymentProof {
205            proof_of_payment: make_proof_of_payment(),
206            tx_hashes: vec![tx1, tx2],
207        };
208
209        let bytes = serialize_single_node_proof(&proof).unwrap();
210        let (_, hashes) = deserialize_proof(&bytes).unwrap();
211
212        assert_eq!(hashes.len(), 2);
213        assert_eq!(hashes.first().unwrap(), &tx1);
214        assert_eq!(hashes.get(1).unwrap(), &tx2);
215    }
216
217    // =========================================================================
218    // detect_proof_type tests
219    // =========================================================================
220
221    #[test]
222    fn test_detect_proof_type_single_node() {
223        let bytes = [PROOF_TAG_SINGLE_NODE, 0x00, 0x01];
224        let result = detect_proof_type(&bytes);
225        assert_eq!(result, Some(ProofType::SingleNode));
226    }
227
228    #[test]
229    fn test_detect_proof_type_merkle() {
230        let bytes = [PROOF_TAG_MERKLE, 0x00, 0x01];
231        let result = detect_proof_type(&bytes);
232        assert_eq!(result, Some(ProofType::Merkle));
233    }
234
235    #[test]
236    fn test_detect_proof_type_unknown_tag() {
237        let bytes = [0xFF, 0x00, 0x01];
238        let result = detect_proof_type(&bytes);
239        assert_eq!(result, None);
240    }
241
242    #[test]
243    fn test_detect_proof_type_empty_bytes() {
244        let bytes: &[u8] = &[];
245        let result = detect_proof_type(bytes);
246        assert_eq!(result, None);
247    }
248
249    // =========================================================================
250    // Tagged serialize/deserialize round-trip tests
251    // =========================================================================
252
253    #[test]
254    fn test_serialize_single_node_proof_roundtrip_with_tag() {
255        let tx_hash = FixedBytes::from([0xCCu8; 32]);
256        let proof = PaymentProof {
257            proof_of_payment: make_proof_of_payment(),
258            tx_hashes: vec![tx_hash],
259        };
260
261        let tagged_bytes = serialize_single_node_proof(&proof).unwrap();
262
263        // First byte must be the single-node tag
264        assert_eq!(
265            tagged_bytes.first().copied(),
266            Some(PROOF_TAG_SINGLE_NODE),
267            "Tagged proof must start with PROOF_TAG_SINGLE_NODE"
268        );
269
270        // detect_proof_type should identify it
271        assert_eq!(
272            detect_proof_type(&tagged_bytes),
273            Some(ProofType::SingleNode)
274        );
275
276        // deserialize_proof handles the tag transparently
277        let (pop, hashes) = deserialize_proof(&tagged_bytes).unwrap();
278        assert_eq!(pop.peer_quotes.len(), 1);
279        assert_eq!(hashes.len(), 1);
280        assert_eq!(hashes.first().unwrap(), &tx_hash);
281    }
282
283    // =========================================================================
284    // Merkle proof serialize/deserialize round-trip tests
285    // =========================================================================
286
287    /// Create a minimal valid `MerklePaymentProof` from a small merkle tree.
288    fn make_test_merkle_proof() -> MerklePaymentProof {
289        let timestamp = std::time::SystemTime::now()
290            .duration_since(std::time::UNIX_EPOCH)
291            .unwrap()
292            .as_secs();
293
294        // Build a tree with 4 addresses (minimal depth)
295        let addresses: Vec<xor_name::XorName> = (0..4u8)
296            .map(|i| xor_name::XorName::from_content(&[i]))
297            .collect();
298        let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
299
300        // Build candidate nodes with ML-DSA-65 signing (matching production)
301        let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
302            std::array::from_fn(|i| {
303                let ml_dsa = MlDsa65::new();
304                let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
305                let price = Amount::from(1024u64);
306                #[allow(clippy::cast_possible_truncation)]
307                let reward_address = RewardsAddress::new([i as u8; 20]);
308                let msg =
309                    MerklePaymentCandidateNode::bytes_to_sign(&price, &reward_address, timestamp);
310                let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
311                let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
312
313                MerklePaymentCandidateNode {
314                    pub_key: pub_key.as_bytes().to_vec(),
315                    price,
316                    reward_address,
317                    merkle_payment_timestamp: timestamp,
318                    signature,
319                }
320            });
321
322        let reward_candidates = tree.reward_candidates(timestamp).unwrap();
323        let midpoint_proof = reward_candidates.first().unwrap().clone();
324
325        let pool = MerklePaymentCandidatePool {
326            midpoint_proof,
327            candidate_nodes,
328        };
329
330        let first_address = *addresses.first().unwrap();
331        let address_proof = tree.generate_address_proof(0, first_address).unwrap();
332
333        MerklePaymentProof::new(first_address, address_proof, pool)
334    }
335
336    #[test]
337    fn test_serialize_merkle_proof_roundtrip() {
338        let merkle_proof = make_test_merkle_proof();
339
340        let tagged_bytes = serialize_merkle_proof(&merkle_proof).unwrap();
341
342        // First byte must be the merkle tag
343        assert_eq!(
344            tagged_bytes.first().copied(),
345            Some(PROOF_TAG_MERKLE),
346            "Tagged merkle proof must start with PROOF_TAG_MERKLE"
347        );
348
349        // detect_proof_type should identify it as merkle
350        assert_eq!(detect_proof_type(&tagged_bytes), Some(ProofType::Merkle));
351
352        // deserialize_merkle_proof should recover the original proof
353        let recovered = deserialize_merkle_proof(&tagged_bytes).unwrap();
354        assert_eq!(recovered.address, merkle_proof.address);
355        assert_eq!(
356            recovered.winner_pool.candidate_nodes.len(),
357            CANDIDATES_PER_POOL
358        );
359    }
360
361    #[test]
362    fn test_deserialize_merkle_proof_rejects_wrong_tag() {
363        let merkle_proof = make_test_merkle_proof();
364        let mut tagged_bytes = serialize_merkle_proof(&merkle_proof).unwrap();
365
366        // Replace the tag with the single-node tag
367        if let Some(first) = tagged_bytes.first_mut() {
368            *first = PROOF_TAG_SINGLE_NODE;
369        }
370
371        let result = deserialize_merkle_proof(&tagged_bytes);
372        assert!(result.is_err(), "Should reject wrong tag byte");
373        let err_msg = result.unwrap_err();
374        assert!(
375            err_msg.contains("Missing merkle proof tag"),
376            "Error should mention missing tag: {err_msg}"
377        );
378    }
379
380    #[test]
381    fn test_deserialize_merkle_proof_rejects_empty() {
382        let result = deserialize_merkle_proof(&[]);
383        assert!(result.is_err());
384    }
385}