Skip to main content

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