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