1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PaymentProof {
18 pub proof_of_payment: ProofOfPayment,
20 pub tx_hashes: Vec<TxHash>,
23 #[serde(default)]
33 pub commitment_sidecars: Vec<Vec<u8>>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum ProofType {
40 SingleNode,
42 Merkle,
44}
45
46#[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
58pub 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
73pub 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
88pub 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
101pub 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
120pub 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 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 #[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 #[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 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 assert_eq!(
303 detect_proof_type(&tagged_bytes),
304 Some(ProofType::SingleNode)
305 );
306
307 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 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 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 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 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 assert_eq!(detect_proof_type(&tagged_bytes), Some(ProofType::Merkle));
389
390 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 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}