1use super::batch::{
7 build_plan_proof, proof_is_safely_fresh, ChunkPaymentPlan, PaidChunk, PreparedChunk,
8};
9use crate::data::error::{Error, Result};
10use ant_protocol::{
11 evm::{QuoteHash, TxHash},
12 payment::deserialize_proof,
13 XorName,
14};
15use serde::{Deserialize, Serialize};
16use std::{
17 collections::HashMap,
18 time::{Duration, SystemTime},
19};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PaymentAttempt {
25 pub merkle: bool,
27 pub addresses: Vec<XorName>,
29 #[serde(default)]
31 pub submissions: Vec<serde_json::Value>,
32 pub receipt: Option<serde_json::Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38struct FailedPaymentAttempt {
39 attempt: PaymentAttempt,
40 resolution: serde_json::Value,
41}
42
43#[derive(Debug, Default, Clone, Serialize, Deserialize)]
46pub struct UploadState {
47 plans: HashMap<XorName, ChunkPaymentPlan>,
48 proofs: HashMap<XorName, Vec<u8>>,
49 #[serde(default)]
50 pub(crate) pending_merkle: Option<super::merkle::PreparedMerkleBatch>,
51 #[serde(default)]
53 pub pending_payment: Option<PaymentAttempt>,
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 failed_payments: Vec<FailedPaymentAttempt>,
56}
57
58pub fn reusable_proof(address: &XorName, bytes: &[u8], now: SystemTime) -> bool {
60 if let Ok(proof) = ant_protocol::payment::deserialize_merkle_proof(bytes) {
61 return proof.address.0 == *address
62 && proof.data_proof.verify()
63 && proof.data_proof.root() == proof.winner_pool.midpoint_proof.root()
64 && merkle_fresh(
65 proof.winner_pool.midpoint_proof.merkle_payment_timestamp,
66 now,
67 );
68 }
69 let Ok((proof, _)) = deserialize_proof(bytes) else {
70 return false;
71 };
72 !proof.peer_quotes.is_empty()
73 && proof
74 .peer_quotes
75 .iter()
76 .all(|(_, quote)| quote.content.0 == *address)
77 && proof_is_safely_fresh(
78 &proof,
79 now,
80 Duration::from_secs(
81 super::batch::CACHED_PROOF_MAX_AGE_SECS
82 - super::batch::CACHED_PROOF_SAFETY_MARGIN_SECS,
83 ),
84 )
85}
86
87pub(crate) fn merkle_fresh(timestamp: u64, now: SystemTime) -> bool {
88 now.duration_since(std::time::UNIX_EPOCH)
89 .ok()
90 .is_some_and(|now| {
91 timestamp <= now.as_secs()
92 && now.as_secs() - timestamp < ant_protocol::evm::MERKLE_PAYMENT_EXPIRATION
93 })
94}
95
96impl UploadState {
97 #[cfg(all(target_arch = "wasm32", feature = "browser-wasm"))]
98 pub(crate) fn record_failed_payment(&mut self, resolution: serde_json::Value) -> Result<()> {
99 let attempt = self
100 .pending_payment
101 .take()
102 .ok_or_else(|| Error::Payment("no pending payment".into()))?;
103 self.failed_payments.push(FailedPaymentAttempt {
104 attempt,
105 resolution,
106 });
107 Ok(())
108 }
109
110 pub(crate) fn start_payment(&mut self, merkle: bool, addresses: Vec<XorName>) -> Result<()> {
111 if self.pending_payment.is_some() {
112 return Err(Error::Payment(
113 "payment outcome unknown; reconcile before submitting again".into(),
114 ));
115 }
116 self.pending_payment = Some(PaymentAttempt {
117 merkle,
118 addresses,
119 submissions: Vec::new(),
120 receipt: None,
121 });
122 Ok(())
123 }
124
125 pub(crate) fn pending_plans(&self) -> Result<Vec<ChunkPaymentPlan>> {
126 let attempt = self
127 .pending_payment
128 .as_ref()
129 .ok_or_else(|| Error::Payment("no pending payment".into()))?;
130 attempt
131 .addresses
132 .iter()
133 .map(|address| {
134 self.plans.get(address).cloned().ok_or_else(|| {
135 Error::InvalidData("pending payment is missing its prepared plan".into())
136 })
137 })
138 .collect()
139 }
140
141 pub(crate) fn insert_merkle(&mut self, result: super::merkle::MerkleBatchPaymentResult) {
142 self.proofs.extend(result.proofs);
143 self.pending_merkle = None;
144 self.pending_payment = None;
145 }
146 pub(crate) fn proof(&self, address: &XorName) -> Option<&Vec<u8>> {
147 self.proofs.get(address)
148 }
149
150 #[cfg(feature = "native")]
151 pub(crate) fn proofs(&self) -> &HashMap<XorName, Vec<u8>> {
152 &self.proofs
153 }
154
155 pub(super) fn pay_prepared(
157 chunk: PreparedChunk,
158 transactions: &HashMap<QuoteHash, TxHash>,
159 ) -> Result<PaidChunk> {
160 let mut state = Self::default();
161 state.prepare(ChunkPaymentPlan {
162 address: chunk.address,
163 data_size: chunk.content.len() as u64,
164 quoted_peers: chunk.quoted_peers.clone(),
165 payment: chunk.payment,
166 peer_quotes: chunk.peer_quotes,
167 commitment_sidecars: chunk.commitment_sidecars,
168 });
169 state.confirm(
170 &[chunk.address],
171 transactions,
172 crate::runtime::system_time(),
173 )?;
174 let proof_bytes = state
175 .proofs
176 .remove(&chunk.address)
177 .ok_or_else(|| Error::Payment("confirmed proof missing".into()))?;
178 Ok(PaidChunk {
179 content: chunk.content,
180 address: chunk.address,
181 quoted_peers: chunk.quoted_peers,
182 proof_bytes,
183 })
184 }
185
186 pub fn from_proofs(proofs: HashMap<XorName, Vec<u8>>) -> Self {
188 Self {
189 plans: HashMap::new(),
190 proofs,
191 pending_merkle: None,
192 pending_payment: None,
193 failed_payments: Vec::new(),
194 }
195 }
196
197 pub fn retained_plan(
200 &self,
201 address: &XorName,
202 size: u64,
203 now: SystemTime,
204 ) -> Option<ChunkPaymentPlan> {
205 if self.is_paid(address, now) {
206 return None;
207 }
208 let plan = self.plans.get(address)?;
209 if plan.data_size != size || plan.peer_quotes.is_empty() {
210 return None;
211 }
212 let proof = ant_protocol::evm::ProofOfPayment {
213 peer_quotes: plan.peer_quotes.clone(),
214 };
215 proof_is_safely_fresh(
216 &proof,
217 now,
218 Duration::from_secs(
219 super::batch::CACHED_PROOF_MAX_AGE_SECS
220 - super::batch::CACHED_PROOF_SAFETY_MARGIN_SECS,
221 ),
222 )
223 .then(|| plan.clone())
224 }
225
226 pub fn prepare(&mut self, plan: ChunkPaymentPlan) {
228 self.plans.insert(plan.address, plan);
229 }
230
231 pub fn is_paid(&self, address: &XorName, now: SystemTime) -> bool {
233 self.proofs
234 .get(address)
235 .is_some_and(|bytes| reusable_proof(address, bytes, now))
236 }
237
238 pub fn confirm(
241 &mut self,
242 addresses: &[XorName],
243 transactions: &HashMap<QuoteHash, TxHash>,
244 now: SystemTime,
245 ) -> Result<()> {
246 let proofs = addresses
247 .iter()
248 .filter(|address| !self.is_paid(address, now))
249 .map(|address| {
250 let plan = self
251 .plans
252 .get(address)
253 .ok_or_else(|| Error::Payment("missing prepared payment plan".into()))?;
254 build_plan_proof(plan, transactions).map(|proof| (*address, proof))
255 })
256 .collect::<Result<Vec<_>>>()?;
257 self.proofs.extend(proofs);
258 if self.pending_payment.as_ref().is_some_and(|attempt| {
259 !attempt.merkle
260 && attempt
261 .addresses
262 .iter()
263 .all(|address| self.proofs.contains_key(address))
264 }) {
265 self.pending_payment = None;
266 }
267 for address in addresses {
268 self.plans.remove(address);
269 }
270 Ok(())
271 }
272
273 pub fn reuse_prepared(&self, prepared: &PreparedChunk, now: SystemTime) -> Option<PaidChunk> {
275 let proof_bytes = self.proofs.get(&prepared.address)?;
276 if !reusable_proof(&prepared.address, proof_bytes, now) {
277 return None;
278 }
279 Some(PaidChunk {
280 content: prepared.content.clone(),
281 address: prepared.address,
282 quoted_peers: prepared.quoted_peers.clone(),
283 proof_bytes: proof_bytes.clone(),
284 })
285 }
286
287 pub fn checkpoint(&self) -> Result<Vec<u8>> {
289 rmp_serde::to_vec_named(self).map_err(|e| Error::Serialization(e.to_string()))
290 }
291
292 pub fn restore(bytes: &[u8]) -> Result<Self> {
294 let state: Self =
295 rmp_serde::from_slice(bytes).map_err(|e| Error::Serialization(e.to_string()))?;
296 for (address, plan) in &state.plans {
297 if address != &plan.address
298 || plan.peer_quotes.is_empty()
299 || plan.peer_quotes.iter().any(|(peer, quote)| {
300 quote.content.0 != *address
301 || *peer
302 != ant_protocol::evm::EncodedPeerId::new(
303 *blake3::hash("e.pub_key).as_bytes(),
304 )
305 || !ant_protocol::payment::verify_quote_signature(quote)
306 })
307 {
308 return Err(Error::InvalidData("invalid checkpoint quote".into()));
309 }
310 let canonical = super::batch::SingleNodeQuotePayment::from_quotes(
311 plan.peer_quotes
312 .iter()
313 .map(|(_, quote)| quote.clone())
314 .collect(),
315 )?;
316 let signature = |quotes: &[ant_protocol::payment::QuotePaymentInfo]| {
317 quotes
318 .iter()
319 .map(|q| (q.quote_hash, q.rewards_address, q.amount, q.price))
320 .collect::<Vec<_>>()
321 };
322 if signature(&canonical.quotes) != signature(&plan.payment.quotes) {
323 return Err(Error::InvalidData(
324 "checkpoint payment differs from signed quotes".into(),
325 ));
326 }
327 }
328 if let Some(prepared) = &state.pending_merkle {
329 prepared.validate_checkpoint()?;
330 }
331 if let Some(attempt) = &state.pending_payment {
332 if attempt.submissions.len() > 256 || (attempt.merkle && state.pending_merkle.is_none())
333 {
334 return Err(Error::InvalidData("invalid payment journal".into()));
335 }
336 if !attempt.merkle {
337 state.pending_plans()?;
338 }
339 }
340 for (address, bytes) in &state.proofs {
341 if let Ok(proof) = ant_protocol::payment::deserialize_merkle_proof(bytes) {
342 if proof.address.0 != *address
343 || !proof.data_proof.verify()
344 || proof.data_proof.root() != proof.winner_pool.midpoint_proof.root()
345 || proof.winner_pool.candidate_nodes.iter().any(|candidate| {
346 !ant_protocol::payment::verify_merkle_candidate_signature(candidate)
347 })
348 {
349 return Err(Error::InvalidData("invalid checkpoint Merkle proof".into()));
350 }
351 continue;
352 }
353 let proof = ant_protocol::payment::proof::deserialize_single_node_proof(bytes)
354 .map_err(Error::InvalidData)?;
355 if proof.tx_hashes.is_empty()
356 || proof.proof_of_payment.peer_quotes.is_empty()
357 || proof
358 .proof_of_payment
359 .peer_quotes
360 .iter()
361 .any(|(peer, quote)| {
362 quote.content.0 != *address
363 || *peer
364 != ant_protocol::evm::EncodedPeerId::new(
365 *blake3::hash("e.pub_key).as_bytes(),
366 )
367 || !ant_protocol::payment::verify_quote_signature(quote)
368 })
369 {
370 return Err(Error::InvalidData(
371 "invalid checkpoint payment proof".into(),
372 ));
373 }
374 }
375 Ok(state)
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::super::batch::{finalize_batch_payment, SingleNodeQuotePayment};
382 use super::*;
383 use ant_protocol::{
384 evm::{Amount, EncodedPeerId, PaymentQuote, RewardsAddress},
385 transport::NodeIdentity,
386 };
387 use bytes::Bytes;
388
389 fn plan(content: &[u8], timestamp: SystemTime) -> ChunkPaymentPlan {
390 let identity = NodeIdentity::generate().unwrap();
391 let mut quote = PaymentQuote {
392 content: xor_name::XorName(ant_protocol::compute_address(content)),
393 timestamp,
394 price: Amount::from(10),
395 rewards_address: RewardsAddress::new([1; 20]),
396 pub_key: identity.public_key().as_bytes().to_vec(),
397 signature: Vec::new(),
398 committed_key_count: 0,
399 commitment_pin: None,
400 };
401 quote.signature = identity
402 .sign("e.bytes_for_sig())
403 .unwrap()
404 .as_bytes()
405 .to_vec();
406 ChunkPaymentPlan {
407 address: quote.content.0,
408 data_size: content.len() as u64,
409 quoted_peers: Vec::new(),
410 payment: SingleNodeQuotePayment::from_quotes(vec![quote.clone()]).unwrap(),
411 peer_quotes: vec![(
412 EncodedPeerId::new(*blake3::hash("e.pub_key).as_bytes()),
413 quote,
414 )],
415 commitment_sidecars: Vec::new(),
416 }
417 }
418
419 #[test]
420 fn checkpoint_reuses_original_proof_with_new_quotes_and_matches_native_finalization() {
421 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
422 let content = Bytes::from_static(b"retained paid record");
423 let original = plan(&content, now);
424 let txs = HashMap::from([(original.payment.quotes[0].quote_hash, TxHash::from([7; 32]))]);
425 let mut state = UploadState::default();
426 state.prepare(original.clone());
427 state = UploadState::restore(&state.checkpoint().unwrap()).unwrap();
428 assert_eq!(
429 state
430 .retained_plan(&original.address, content.len() as u64, now)
431 .unwrap()
432 .payment
433 .quotes[0]
434 .quote_hash,
435 original.payment.quotes[0].quote_hash
436 );
437 state.confirm(&[original.address], &txs, now).unwrap();
438 state = UploadState::restore(&state.checkpoint().unwrap()).unwrap();
439 let refreshed = plan(&content, now + Duration::from_secs(1));
440 assert_ne!(
441 refreshed.payment.quotes[0].quote_hash,
442 original.payment.quotes[0].quote_hash
443 );
444 let recovered = state
445 .reuse_prepared(&refreshed.with_content(content.clone()).unwrap(), now)
446 .unwrap();
447 let native =
448 finalize_batch_payment(vec![original.with_content(content).unwrap()], &txs).unwrap();
449 assert_eq!(recovered.proof_bytes, native[0].proof_bytes);
450 }
451
452 #[test]
453 fn confirmation_is_atomic_and_supports_distinct_transactions() {
454 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
455 let a = plan(b"first", now);
456 let b = plan(b"second", now);
457 let mut state = UploadState::default();
458 state.prepare(a.clone());
459 state.prepare(b.clone());
460 let mut txs = HashMap::from([(a.payment.quotes[0].quote_hash, TxHash::from([1; 32]))]);
461 assert!(state.confirm(&[a.address, b.address], &txs, now).is_err());
462 assert!(!state.is_paid(&a.address, now));
463 txs.insert(b.payment.quotes[0].quote_hash, TxHash::from([2; 32]));
464 state.confirm(&[a.address, b.address], &txs, now).unwrap();
465 assert!(state.is_paid(&a.address, now));
466 assert!(state.is_paid(&b.address, now));
467 assert!(!state.is_paid(&a.address, now + Duration::from_secs(24 * 60 * 60 - 299)));
468 assert!(!reusable_proof(&b.address, &state.proofs[&a.address], now));
469 }
470
471 #[test]
472 fn checkpoint_rejects_modified_payment_amount() {
473 let mut state = UploadState::default();
474 let mut a = plan(b"first", SystemTime::UNIX_EPOCH);
475 a.payment.quotes[0].amount += Amount::from(1);
476 state.prepare(a);
477 assert!(UploadState::restore(&state.checkpoint().unwrap()).is_err());
478 }
479 #[test]
480 fn expired_proof_allows_a_fresh_plan_without_discarding_payment_evidence() {
481 let issued = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
482 let old = plan(b"expired upload", issued);
483 let mut state = UploadState::default();
484 state.prepare(old.clone());
485 state
486 .confirm(
487 &[old.address],
488 &HashMap::from([(old.payment.quotes[0].quote_hash, TxHash::from([7; 32]))]),
489 issued,
490 )
491 .unwrap();
492 let evidence = state.proof(&old.address).cloned().unwrap();
493 let now = issued + Duration::from_secs(25 * 60 * 60);
494 let fresh = plan(b"expired upload", now);
495 state.prepare(fresh.clone());
496 let state = UploadState::restore(&state.checkpoint().unwrap()).unwrap();
497 assert!(state
498 .retained_plan(&old.address, old.data_size, issued)
499 .is_none());
500 assert!(state
501 .retained_plan(&fresh.address, fresh.data_size, now)
502 .is_some());
503 assert_eq!(state.proof(&old.address), Some(&evidence));
504 }
505}