1use super::*;
14use crate::data::client::adaptive::observe_op;
15use crate::data::client::batch::{
16 finalize_batch_payment, PaidChunk, PaymentIntent, PreparedChunk, WaveAggregateStats, WaveResult,
17};
18use crate::data::client::chunk::{ChunkFetchDiagnostics, ChunkPeerGetResult};
19use crate::data::client::classify_error;
20use crate::data::client::diagnostics::DownloadDiagnosticsSender;
21use crate::data::client::merkle::{
22 finalize_merkle_batch, merge_merkle_batch_results, merkle_batch_sizes, merkle_billable_leaves,
23 merkle_deferred_retry, merkle_store_with_retry, should_use_merkle, MerkleBatchPaymentResult,
24 PaymentMode, PreparedMerkleBatch, DEFERRED_ROUND_DELAYS_SECS,
25};
26use crate::data::client::Client;
27use crate::data::error::{Error, PartialUploadSpend, Result};
28use ant_protocol::evm::{Amount, PaymentQuote, QuoteHash, TxHash, MAX_LEAVES};
29use ant_protocol::transport::{MultiAddr, PeerId};
30use ant_protocol::{compute_address, XorName as ChunkAddress, DATA_TYPE_CHUNK};
31use bytes::Bytes;
32use fs2::FileExt;
33use futures::stream::StreamExt;
34use self_encryption::{
35 stream_decrypt_batch_size, stream_encrypt, streaming_decrypt_with_batch_size, DataMap,
36};
37use std::collections::{HashMap, HashSet};
38use std::io::Write;
39use std::num::NonZeroUsize;
40use std::path::{Path, PathBuf};
41use std::sync::{Arc, Mutex};
42use tokio::runtime::Handle;
43use tokio::sync::mpsc;
44use tracing::{debug, info, warn};
45use xor_name::XorName;
46
47type QuoteEntry = (
52 PeerId,
53 Vec<MultiAddr>,
54 PaymentQuote,
55 Amount,
56 Option<Vec<u8>>,
57);
58
59type DownloadBatchEntry = (usize, std::result::Result<Bytes, XorName>);
60
61#[derive(Debug, Clone)]
62struct RecordedFileChunkPeerSweep {
63 index: usize,
64 address: ChunkAddress,
65 sweep: FileChunkPeerSweepReport,
66}
67
68#[derive(Clone)]
69struct FileDownloadFetchContext {
70 total_chunks: usize,
71 peer_count: usize,
72 fetched_ref: Arc<std::sync::atomic::AtomicUsize>,
73 progress_ref: Option<mpsc::Sender<DownloadEvent>>,
74 peer_reports: Option<Arc<Mutex<Vec<RecordedFileChunkPeerSweep>>>>,
75 diagnostics: Option<DownloadDiagnosticsSender>,
79}
80
81const UPLOAD_WAVE_SIZE: usize = super::super::batch::PAYMENT_WAVE_SIZE;
83
84const MERKLE_STORE_MAX_IN_FLIGHT: usize = 64;
93
94fn merkle_store_cap(limiter_current: usize) -> usize {
97 limiter_current.clamp(1, MERKLE_STORE_MAX_IN_FLIGHT)
98}
99
100const DOWNLOAD_STREAM_BATCH_FETCH_MULTIPLIER: usize = 4;
104
105const DOWNLOAD_STREAM_BATCH_MEMORY_BUDGET_DIVISOR: u64 = 4;
107
108const DOWNLOAD_STREAM_BATCH_BYTES_PER_CHUNK_MULTIPLIER: u64 = 3;
112
113const ESTIMATE_SAMPLE_CAP: usize = 5;
120
121const FIRST_DIAGNOSTIC_FETCH_ATTEMPT: usize = 1;
123
124fn distributed_sample_indices(total: usize, cap: usize) -> Vec<usize> {
137 if total == 0 {
138 return Vec::new();
139 }
140 let sample_limit = total.min(cap);
141 if sample_limit <= 1 {
142 return vec![0];
143 }
144 let mut indices: Vec<usize> = (0..sample_limit)
145 .map(|i| i * (total - 1) / (sample_limit - 1))
146 .collect();
147 indices.dedup(); indices
149}
150
151fn file_chunk_sweep_report_from_peer_results(
152 attempt: usize,
153 deferred_retry: bool,
154 results: &[ChunkPeerGetResult],
155) -> (Option<Bytes>, FileChunkPeerSweepReport) {
156 let mut content = None;
157 let peers = results
158 .iter()
159 .map(|result| {
160 if content.is_none() {
161 if let Ok(Some(chunk)) = &result.chunk_result {
162 content = Some(chunk.content.clone());
163 }
164 }
165
166 FileChunkPeerReportPeer {
167 peer_id: result.peer_id,
168 peer_addrs: result.peer_addrs.clone(),
169 xor_distance: result.xor_distance,
170 status: file_chunk_peer_status(&result.chunk_result),
171 }
172 })
173 .collect();
174
175 (
176 content,
177 FileChunkPeerSweepReport {
178 attempt,
179 deferred_retry,
180 error: None,
181 peers,
182 },
183 )
184}
185
186fn file_chunk_sweep_report_from_error(
187 attempt: usize,
188 deferred_retry: bool,
189 error: &Error,
190) -> FileChunkPeerSweepReport {
191 FileChunkPeerSweepReport {
192 attempt,
193 deferred_retry,
194 error: Some(error.to_string()),
195 peers: Vec::new(),
196 }
197}
198
199fn file_chunk_reports_from_recorded_sweeps(
200 mut sweeps: Vec<RecordedFileChunkPeerSweep>,
201) -> Vec<FileChunkPeerReport> {
202 sweeps.sort_by_key(|record| (record.index, record.sweep.attempt));
203
204 let mut reports: Vec<FileChunkPeerReport> = Vec::new();
205 for record in sweeps {
206 if let Some(report) = reports
207 .last_mut()
208 .filter(|report| report.index == record.index)
209 {
210 report.sweeps.push(record.sweep);
211 continue;
212 }
213
214 reports.push(FileChunkPeerReport {
215 index: record.index,
216 address: record.address,
217 sweeps: vec![record.sweep],
218 });
219 }
220
221 reports
222}
223
224fn file_chunk_peer_status(
225 chunk_result: &std::result::Result<Option<ant_protocol::DataChunk>, Error>,
226) -> FileChunkPeerStatus {
227 match chunk_result {
228 Ok(Some(chunk)) => FileChunkPeerStatus::Found {
229 bytes: chunk.content.len(),
230 },
231 Ok(None) => FileChunkPeerStatus::NotFound,
232 Err(Error::Timeout(e)) => FileChunkPeerStatus::Timeout { message: e.clone() },
233 Err(Error::Network(e)) => FileChunkPeerStatus::NetworkError { message: e.clone() },
234 Err(e) => FileChunkPeerStatus::Error {
235 message: e.to_string(),
236 },
237 }
238}
239
240const GAS_PER_WAVE_TX: u128 = 1_500_000;
249
250const GAS_PER_MERKLE_TX: u128 = 500_000;
255
256const ARBITRUM_GAS_PRICE_WEI: u128 = 100_000_000;
264
265const DISK_SPACE_HEADROOM_PERCENT: u64 = 10;
271
272const SPILL_STALE_GRACE_SECS: u64 = 30;
289
290const SPILL_DIR_PREFIX: &str = "spill_";
292
293const SPILL_LOCK_NAME: &str = ".lock";
295
296struct ChunkSpill {
297 dir: PathBuf,
299 _lock: std::fs::File,
301 addresses: Vec<[u8; 32]>,
303 seen: HashSet<[u8; 32]>,
305 sizes: HashMap<[u8; 32], u64>,
307 total_bytes: u64,
309}
310
311impl ChunkSpill {
312 fn spill_root() -> Result<PathBuf> {
314 use crate::config;
315 let root = config::data_dir()
316 .map_err(|e| Error::Config(format!("cannot determine data dir for spill: {e}")))?
317 .join("spill");
318 Ok(root)
319 }
320
321 fn new() -> Result<Self> {
327 let root = Self::spill_root()?;
328 std::fs::create_dir_all(&root)?;
329
330 Self::cleanup_stale(&root);
332
333 let now = std::time::SystemTime::now()
334 .duration_since(std::time::UNIX_EPOCH)
335 .unwrap_or_default()
336 .as_secs();
337 let unique: u64 = rand::random();
338 let dir = root.join(format!("{SPILL_DIR_PREFIX}{now}_{unique}"));
339 std::fs::create_dir(&dir)?;
340
341 let lock_path = dir.join(SPILL_LOCK_NAME);
344 let lock_file = std::fs::File::create(&lock_path).map_err(|e| {
345 Error::Io(std::io::Error::new(
346 e.kind(),
347 format!("failed to create spill lockfile: {e}"),
348 ))
349 })?;
350 lock_file.try_lock_exclusive().map_err(|e| {
351 Error::Io(std::io::Error::new(
352 e.kind(),
353 format!("failed to lock spill lockfile: {e}"),
354 ))
355 })?;
356
357 Ok(Self {
358 dir,
359 _lock: lock_file,
360 addresses: Vec::new(),
361 seen: HashSet::new(),
362 sizes: HashMap::new(),
363 total_bytes: 0,
364 })
365 }
366
367 fn cleanup_stale(root: &Path) {
382 let now = std::time::SystemTime::now()
383 .duration_since(std::time::UNIX_EPOCH)
384 .unwrap_or_default()
385 .as_secs();
386
387 if now == 0 {
388 warn!("System clock before Unix epoch, skipping spill cleanup");
391 return;
392 }
393
394 let entries = match std::fs::read_dir(root) {
395 Ok(entries) => entries,
396 Err(_) => return,
397 };
398
399 for entry in entries.flatten() {
400 let name = entry.file_name();
401 let name_str = name.to_string_lossy();
402
403 let suffix = match name_str.strip_prefix(SPILL_DIR_PREFIX) {
405 Some(s) => s,
406 None => continue,
407 };
408
409 let timestamp: u64 = match suffix.split('_').next().and_then(|s| s.parse().ok()) {
411 Some(ts) => ts,
412 None => continue,
413 };
414
415 if now.saturating_sub(timestamp) < SPILL_STALE_GRACE_SECS {
416 continue;
417 }
418
419 let file_type = match entry.file_type() {
421 Ok(ft) => ft,
422 Err(_) => continue,
423 };
424 if !file_type.is_dir() {
425 continue;
426 }
427
428 let path = entry.path();
429
430 let lock_path = path.join(SPILL_LOCK_NAME);
432 if let Ok(lock_file) = std::fs::File::open(&lock_path) {
433 use fs2::FileExt;
434 if lock_file.try_lock_exclusive().is_err() {
435 debug!("Skipping active spill dir: {}", path.display());
437 continue;
438 }
439 drop(lock_file);
442 }
443
444 info!("Cleaning up stale spill dir: {}", path.display());
445 if let Err(e) = std::fs::remove_dir_all(&path) {
446 warn!("Failed to clean up stale spill dir {}: {e}", path.display());
447 }
448 }
449 }
450
451 #[allow(dead_code)]
453 pub(crate) fn run_cleanup() {
454 if let Ok(root) = Self::spill_root() {
455 Self::cleanup_stale(&root);
456 }
457 }
458
459 fn push(&mut self, content: &[u8]) -> Result<()> {
465 let address = compute_address(content);
466 if !self.seen.insert(address) {
467 return Ok(());
468 }
469 let path = self.dir.join(hex::encode(address));
470 std::fs::write(&path, content)?;
471 let content_len = content.len() as u64;
472 self.sizes.insert(address, content_len);
473 self.total_bytes += content_len;
474 self.addresses.push(address);
475 Ok(())
476 }
477
478 fn len(&self) -> usize {
480 self.addresses.len()
481 }
482
483 fn total_bytes(&self) -> u64 {
485 self.total_bytes
486 }
487
488 fn chunk_entries(&self) -> Result<Vec<([u8; 32], u64)>> {
490 self.addresses
491 .iter()
492 .map(|address| {
493 self.sizes
494 .get(address)
495 .copied()
496 .map(|size| (*address, size))
497 .ok_or_else(|| {
498 Error::Storage(format!(
499 "missing size for spilled chunk {}",
500 hex::encode(address)
501 ))
502 })
503 })
504 .collect()
505 }
506
507 fn read_chunk(&self, address: &[u8; 32]) -> Result<Bytes> {
509 let path = self.dir.join(hex::encode(address));
510 let data = std::fs::read(&path).map_err(|e| {
511 Error::Io(std::io::Error::new(
512 e.kind(),
513 format!("reading spilled chunk {}: {e}", hex::encode(address)),
514 ))
515 })?;
516 Ok(Bytes::from(data))
517 }
518
519 fn read_chunks(&self, addresses: &[[u8; 32]]) -> Result<Vec<Bytes>> {
521 addresses.iter().map(|addr| self.read_chunk(addr)).collect()
522 }
523
524 fn read_all_chunks(&self) -> Result<Vec<Bytes>> {
526 self.read_chunks(&self.addresses)
527 }
528
529 fn cleanup(&self) {
531 if let Err(e) = std::fs::remove_dir_all(&self.dir) {
532 warn!(
533 "Failed to clean up chunk spill dir {}: {e}",
534 self.dir.display()
535 );
536 }
537 }
538}
539
540impl Drop for ChunkSpill {
541 fn drop(&mut self) {
542 self.cleanup();
543 }
544}
545
546#[cfg(test)]
547fn cached_merkle_covers_addresses(
548 cached: &MerkleBatchPaymentResult,
549 addresses: &[[u8; 32]],
550) -> bool {
551 addresses
552 .iter()
553 .all(|addr| cached.proofs.contains_key(addr))
554}
555
556fn partition_addresses_by_proof(
567 addresses: &[[u8; 32]],
568 proofs: &HashMap<[u8; 32], Vec<u8>>,
569) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) {
570 addresses
571 .iter()
572 .copied()
573 .partition(|addr| proofs.contains_key(addr))
574}
575
576fn proofless_clause(proofless_count: usize, payment_refusal: Option<&str>) -> Option<String> {
592 if proofless_count == 0 {
593 return None;
594 }
595 Some(match payment_refusal {
596 Some(refusal) => format!(
597 "{proofless_count} chunk(s) have no merkle proof because storers refused this \
598 client's settlement version during payment. That refusal covers those chunks; \
599 any spend reported here settled for earlier sub-batches. {refusal}"
600 ),
601 None => format!("{proofless_count} chunk(s) have no merkle proof"),
602 })
603}
604
605fn merkle_partial_reason(
613 failed_count: usize,
614 proofless_count: usize,
615 total_attempts: usize,
616 payment_refusal: Option<&str>,
617) -> String {
618 let quorum = |n: usize| format!("{n} chunk(s) short of quorum after {total_attempts} attempts");
619 match proofless_clause(proofless_count, payment_refusal) {
620 None => quorum(failed_count),
621 Some(proofless) => match failed_count.saturating_sub(proofless_count) {
625 0 => proofless,
626 short => format!("{}; {proofless}", quorum(short)),
627 },
628 }
629}
630
631fn merkle_fatal_reason(
638 abort: &str,
639 proofless_count: usize,
640 payment_refusal: Option<&str>,
641) -> String {
642 match proofless_clause(proofless_count, payment_refusal) {
643 Some(proofless) => format!("{abort}; {proofless}"),
644 None => abort.to_string(),
645 }
646}
647
648fn partial_upload_after_fatal(
662 addresses: &[[u8; 32]],
663 stored_addresses: Vec<[u8; 32]>,
664 stored_count: usize,
665 total_chunks: usize,
666 known_failed: Vec<([u8; 32], String)>,
667 spend: PartialUploadSpend,
668 reason: String,
669) -> Error {
670 let stored_set: HashSet<[u8; 32]> = stored_addresses.iter().copied().collect();
671 let mut failed_map: HashMap<[u8; 32], String> = HashMap::new();
672 for (addr, msg) in known_failed {
673 if !stored_set.contains(&addr) {
674 failed_map.entry(addr).or_insert(msg);
675 }
676 }
677 for addr in addresses {
678 if !stored_set.contains(addr) {
679 failed_map.entry(*addr).or_insert_with(|| reason.clone());
680 }
681 }
682 let failed: Vec<([u8; 32], String)> = failed_map.into_iter().collect();
683 let failed_count = failed.len();
684 Error::PartialUpload {
685 stored: stored_addresses,
686 stored_count,
687 failed,
688 failed_count,
689 total_chunks,
690 spend: Box::new(spend),
691 reason,
692 }
693}
694
695fn require_fully_paid_for_resumable(winner_pool_hashes: &[Option<[u8; 32]>]) -> Result<()> {
707 let unpaid = winner_pool_hashes.iter().filter(|h| h.is_none()).count();
708 if unpaid > 0 {
709 return Err(Error::Payment(format!(
710 "{unpaid}/{} sub-batch(es) unpaid: the resumable finalize requires every \
711 sub-batch to be paid, because a resume handle cannot acquire proofs for \
712 unpaid chunks and would never drain to Complete. Pay every sub-batch, or \
713 use finalize_upload_merkle_multi() to finalize a partial payment (its \
714 unpaid chunks are reported through PartialUpload).",
715 winner_pool_hashes.len()
716 )));
717 }
718 Ok(())
719}
720
721fn fold_external_merkle_payments(
732 prepared_batches: Vec<PreparedMerkleBatch>,
733 winner_pool_hashes: Vec<Option<[u8; 32]>>,
734) -> Result<MerkleBatchPaymentResult> {
735 let batch_count = prepared_batches.len();
736 if winner_pool_hashes.len() != batch_count {
737 return Err(Error::Payment(format!(
738 "Expected {batch_count} winner pool hash entries (one per \
739 prepared sub-batch), got {}.",
740 winner_pool_hashes.len()
741 )));
742 }
743
744 let mut paid = Vec::with_capacity(batch_count);
745 let mut unpaid_batches = 0usize;
746 for (batch, hash) in prepared_batches.into_iter().zip(winner_pool_hashes) {
747 match hash {
748 Some(h) => paid.push(finalize_merkle_batch(batch, h)?),
749 None => unpaid_batches += 1,
750 }
751 }
752 if paid.is_empty() {
753 return Err(Error::Payment(
754 "No merkle sub-batch was paid — nothing to finalize. \
755 Pay at least one batch or drop the prepared upload."
756 .to_string(),
757 ));
758 }
759 if unpaid_batches > 0 {
760 warn!(
761 "External merkle finalize: {unpaid_batches}/{batch_count} sub-batch(es) \
762 unpaid; their chunks will be reported as failed"
763 );
764 }
765 Ok(merge_merkle_batch_results(paid))
766}
767
768fn assemble_merkle_finalize_outcome(
780 store_result: Result<(usize, String, u128, WaveAggregateStats)>,
781 data_map: DataMap,
782 data_map_address: Option<[u8; 32]>,
783 total_chunks: usize,
784 chunk_store: ExternalChunkStore,
785 batch_result: MerkleBatchPaymentResult,
786) -> Result<FinalizeOutcome> {
787 match store_result {
788 Ok((chunks_stored, _storage_cost, _gas_cost, stats)) => {
789 info!("External-signer merkle upload finalized: {chunks_stored} chunks stored");
790 Ok(FinalizeOutcome::Complete(FileUploadResult {
791 data_map,
792 chunks_stored,
793 chunks_failed: 0,
794 total_chunks,
795 payment_mode_used: PaymentMode::Merkle,
796 storage_cost_atto: "0".into(),
799 gas_cost_wei: 0,
800 data_map_address,
801 chunk_attempts_total: stats.chunk_attempts_total,
802 store_durations_ms: stats.store_durations_ms,
803 retries_histogram: stats.retries_histogram,
804 }))
805 }
806 Err(Error::PartialUpload {
807 stored,
808 stored_count,
809 failed,
810 failed_count,
811 spend,
812 ..
813 }) => {
814 let unstored_addresses: Vec<[u8; 32]> = failed.iter().map(|(addr, _)| *addr).collect();
817 let result = FileUploadResult {
818 data_map: data_map.clone(),
819 chunks_stored: stored_count,
820 chunks_failed: failed_count,
821 total_chunks,
822 payment_mode_used: PaymentMode::Merkle,
823 storage_cost_atto: spend.storage_cost_atto.clone(),
824 gas_cost_wei: spend.gas_cost_wei,
825 data_map_address,
826 chunk_attempts_total: 0,
828 store_durations_ms: Vec::new(),
829 retries_histogram: [0; 4],
830 };
831 let resume = MerkleFinalizeResume {
832 data_map,
833 data_map_address,
834 total_chunks,
835 chunk_store,
836 unstored_addresses,
837 batch_result,
838 stored_addresses: stored,
841 };
842 Ok(FinalizeOutcome::Partial {
843 result,
844 resume: FinalizeResume::Merkle(Box::new(resume)),
845 })
846 }
847 Err(e) => Err(e),
848 }
849}
850
851fn assemble_wave_finalize_outcome(
862 wave_result: WaveResult,
863 mut retained: HashMap<[u8; 32], PaidChunk>,
864 data_map: DataMap,
865 data_map_address: Option<[u8; 32]>,
866 total_chunks: usize,
867 already_stored_count: usize,
868 storage_cost_atto: String,
869) -> FinalizeOutcome {
870 let stored_count = already_stored_count + wave_result.stored.len();
871 if wave_result.failed.is_empty() {
872 info!("External-signer upload finalized: {stored_count} chunks stored");
873 let mut stats = WaveAggregateStats::default();
874 stats.absorb(&wave_result);
875 return FinalizeOutcome::Complete(FileUploadResult {
876 data_map,
877 chunks_stored: stored_count,
878 chunks_failed: 0,
879 total_chunks,
880 payment_mode_used: PaymentMode::Single,
881 storage_cost_atto,
884 gas_cost_wei: 0,
885 data_map_address,
886 chunk_attempts_total: stats.chunk_attempts_total,
887 store_durations_ms: stats.store_durations_ms,
888 retries_histogram: stats.retries_histogram,
889 });
890 }
891
892 let failed_count = wave_result.failed.len();
895 let failed_paid_chunks: Vec<PaidChunk> = wave_result
896 .failed
897 .iter()
898 .filter_map(|(addr, _)| retained.remove(addr))
899 .collect();
900 let result = FileUploadResult {
901 data_map: data_map.clone(),
902 chunks_stored: stored_count,
903 chunks_failed: failed_count,
904 total_chunks,
905 payment_mode_used: PaymentMode::Single,
906 storage_cost_atto: storage_cost_atto.clone(),
907 gas_cost_wei: 0,
908 data_map_address,
909 chunk_attempts_total: 0,
911 store_durations_ms: Vec::new(),
912 retries_histogram: [0; 4],
913 };
914 let resume = WaveFinalizeResume {
915 data_map,
916 data_map_address,
917 total_chunks,
918 stored_count,
919 failed_paid_chunks,
920 storage_cost_atto,
921 };
922 FinalizeOutcome::Partial {
923 result,
924 resume: FinalizeResume::Wave(Box::new(resume)),
925 }
926}
927
928#[derive(Debug)]
931#[cfg(test)]
932struct SingleWaveOutcome {
933 stored: Vec<[u8; 32]>,
935 failed: Vec<([u8; 32], String)>,
937 storage_atto: Amount,
939 gas_wei: u128,
941 stats: WaveAggregateStats,
944}
945
946#[cfg(test)]
957fn fold_single_wave(
958 result: Result<(Vec<[u8; 32]>, String, u128, WaveAggregateStats)>,
959) -> Result<SingleWaveOutcome> {
960 match result {
961 Ok((stored, storage, gas, stats)) => Ok(SingleWaveOutcome {
962 stored,
963 failed: Vec::new(),
964 storage_atto: storage.parse().unwrap_or(Amount::ZERO),
965 gas_wei: gas,
966 stats,
967 }),
968 Err(Error::PartialUpload {
969 stored,
970 failed,
971 spend,
972 ..
973 }) => Ok(SingleWaveOutcome {
974 stored,
975 failed,
976 storage_atto: spend.storage_cost_atto.parse().unwrap_or(Amount::ZERO),
977 gas_wei: spend.gas_cost_wei,
978 stats: WaveAggregateStats::default(),
979 }),
980 Err(e) => Err(e),
981 }
982}
983
984#[allow(clippy::too_many_arguments)]
996#[cfg(test)]
997fn settlement_refusal_after_paid_waves(
998 refusal: &str,
999 wave_num: usize,
1000 wave_count: usize,
1001 stored_addresses: Vec<[u8; 32]>,
1002 total_stored: usize,
1003 remaining: &[[u8; 32]],
1004 total_chunks: usize,
1005 total_storage: Amount,
1006 total_gas: u128,
1007) -> Error {
1008 let remaining_count = remaining.len();
1009 let refused_note = format!(
1010 "not quoted: storers refused this client's settlement version at wave \
1011 {wave_num}/{wave_count}"
1012 );
1013 let failed: Vec<([u8; 32], String)> = remaining
1014 .iter()
1015 .map(|addr| (*addr, refused_note.clone()))
1016 .collect();
1017 Error::PartialUpload {
1018 stored: stored_addresses,
1019 stored_count: total_stored,
1020 failed,
1021 failed_count: remaining_count,
1022 total_chunks,
1023 spend: Box::new(PartialUploadSpend {
1024 storage_cost_atto: total_storage.to_string(),
1025 gas_cost_wei: total_gas,
1026 }),
1027 reason: format!(
1028 "storers refused this client's settlement version at wave {wave_num}/{wave_count}: \
1029 the {total_stored} chunk(s) in earlier wave(s) were already paid for and stored, \
1030 and the remaining {remaining_count} chunk(s) were neither quoted nor paid. {refusal}"
1031 ),
1032 }
1033}
1034
1035fn check_disk_space_for_spill(file_size: u64) -> Result<()> {
1040 let spill_root = ChunkSpill::spill_root()?;
1041
1042 std::fs::create_dir_all(&spill_root)?;
1044
1045 let available = fs2::available_space(&spill_root).map_err(|e| {
1046 Error::Io(std::io::Error::new(
1047 e.kind(),
1048 format!(
1049 "failed to query disk space on {}: {e}",
1050 spill_root.display()
1051 ),
1052 ))
1053 })?;
1054
1055 let headroom = file_size / DISK_SPACE_HEADROOM_PERCENT;
1057 let required = file_size.saturating_add(headroom);
1058
1059 if available < required {
1060 let avail_mb = available / (1024 * 1024);
1061 let req_mb = required / (1024 * 1024);
1062 return Err(Error::InsufficientDiskSpace(format!(
1063 "need ~{req_mb} MB in spill dir ({}) but only {avail_mb} MB available",
1064 spill_root.display()
1065 )));
1066 }
1067
1068 debug!(
1069 "Disk space check passed: {available} bytes available, {required} bytes required (spill: {})",
1070 spill_root.display()
1071 );
1072 Ok(())
1073}
1074
1075fn usable_memory_bytes() -> Option<u64> {
1076 let mut system = sysinfo::System::new();
1077 system.refresh_memory();
1078
1079 let available_memory = system.available_memory();
1080 let free_memory = system.free_memory();
1081 let used_memory = system.used_memory();
1082 let total_memory = system.total_memory();
1083 let unused_memory = total_memory.saturating_sub(used_memory);
1084
1085 let mut usable = [available_memory, free_memory, unused_memory]
1086 .into_iter()
1087 .filter(|bytes| *bytes > 0)
1088 .max();
1089
1090 let cgroup_free_memory = system
1091 .cgroup_limits()
1092 .filter(|limits| limits.total_memory > 0)
1093 .map(|limits| limits.free_memory);
1094 if let Some(cgroup_free_memory) = cgroup_free_memory {
1095 usable = Some(usable.unwrap_or(u64::MAX).min(cgroup_free_memory));
1096 }
1097
1098 debug!(
1099 available_memory,
1100 free_memory,
1101 used_memory,
1102 total_memory,
1103 cgroup_free_memory,
1104 usable_memory = ?usable,
1105 "Detected usable memory for stream decrypt batch sizing"
1106 );
1107
1108 usable
1109}
1110
1111fn stream_decrypt_batch_memory_cap(usable_memory_bytes: u64) -> usize {
1112 let budget = usable_memory_bytes / DOWNLOAD_STREAM_BATCH_MEMORY_BUDGET_DIVISOR;
1113 let estimated_bytes_per_chunk = (self_encryption::MAX_CHUNK_SIZE as u64)
1114 .saturating_mul(DOWNLOAD_STREAM_BATCH_BYTES_PER_CHUNK_MULTIPLIER)
1115 .max(1);
1116 let cap = (budget / estimated_bytes_per_chunk).max(1);
1117
1118 usize::try_from(cap).unwrap_or(usize::MAX)
1119}
1120
1121fn adaptive_stream_decrypt_batch_size(
1122 total_chunks: usize,
1123 fetch_cap: usize,
1124 configured_batch_floor: usize,
1125 usable_memory_bytes: Option<u64>,
1126) -> usize {
1127 let fetch_target = fetch_cap
1128 .max(1)
1129 .saturating_mul(DOWNLOAD_STREAM_BATCH_FETCH_MULTIPLIER);
1130 let requested = match usable_memory_bytes {
1131 Some(bytes) => {
1132 let memory_cap = stream_decrypt_batch_memory_cap(bytes);
1133 configured_batch_floor
1134 .max(fetch_target)
1135 .max(1)
1136 .min(memory_cap)
1137 }
1138 None => configured_batch_floor.max(1),
1139 };
1140
1141 requested.min(total_chunks.max(1)).max(1)
1142}
1143
1144#[allow(clippy::large_enum_variant)]
1152#[derive(Debug)]
1153pub enum ExternalPaymentInfo {
1154 WaveBatch {
1156 prepared_chunks: Vec<PreparedChunk>,
1158 payment_intent: PaymentIntent,
1160 },
1161 Merkle {
1163 prepared_batches: Vec<PreparedMerkleBatch>,
1171 chunk_store: ExternalChunkStore,
1174 chunk_addresses: Vec<[u8; 32]>,
1176 },
1177}
1178
1179pub struct ExternalChunkStore(ChunkSpill);
1189
1190impl ExternalChunkStore {
1191 fn from_spill(spill: ChunkSpill) -> Self {
1192 Self(spill)
1193 }
1194
1195 fn spill(&self) -> &ChunkSpill {
1196 &self.0
1197 }
1198}
1199
1200impl std::fmt::Debug for ExternalChunkStore {
1201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1202 f.debug_struct("ExternalChunkStore")
1203 .field("chunks", &self.0.len())
1204 .field("bytes", &self.0.total_bytes())
1205 .finish()
1206 }
1207}
1208
1209#[derive(Debug)]
1222#[non_exhaustive]
1223pub struct PreparedUpload {
1224 pub data_map: DataMap,
1226 pub payment_info: ExternalPaymentInfo,
1231 pub data_map_address: Option<[u8; 32]>,
1238 pub already_stored_addresses: Vec<[u8; 32]>,
1241 pub total_chunks: usize,
1243}
1244
1245#[derive(Debug)]
1258pub enum FinalizeOutcome {
1259 Complete(FileUploadResult),
1261 Partial {
1263 result: FileUploadResult,
1268 resume: FinalizeResume,
1271 },
1272}
1273
1274#[derive(Debug)]
1285#[non_exhaustive]
1286pub enum FinalizeResume {
1287 Wave(Box<WaveFinalizeResume>),
1289 Merkle(Box<MerkleFinalizeResume>),
1291}
1292
1293#[non_exhaustive]
1305pub struct WaveFinalizeResume {
1306 data_map: DataMap,
1307 data_map_address: Option<[u8; 32]>,
1308 total_chunks: usize,
1309 stored_count: usize,
1310 failed_paid_chunks: Vec<PaidChunk>,
1311 storage_cost_atto: String,
1312}
1313
1314impl std::fmt::Debug for WaveFinalizeResume {
1315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1316 f.debug_struct("WaveFinalizeResume")
1317 .field("total_chunks", &self.total_chunks)
1318 .field("stored", &self.stored_count)
1319 .field("unstored", &self.failed_paid_chunks.len())
1320 .field("public", &self.data_map_address.is_some())
1321 .finish_non_exhaustive()
1322 }
1323}
1324
1325#[non_exhaustive]
1338pub struct MerkleFinalizeResume {
1339 data_map: DataMap,
1340 data_map_address: Option<[u8; 32]>,
1341 total_chunks: usize,
1342 chunk_store: ExternalChunkStore,
1343 unstored_addresses: Vec<[u8; 32]>,
1344 batch_result: MerkleBatchPaymentResult,
1345 stored_addresses: Vec<[u8; 32]>,
1346}
1347
1348impl std::fmt::Debug for MerkleFinalizeResume {
1349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1350 f.debug_struct("MerkleFinalizeResume")
1351 .field("total_chunks", &self.total_chunks)
1352 .field("stored", &self.stored_addresses.len())
1353 .field("unstored", &self.unstored_addresses.len())
1354 .field("public", &self.data_map_address.is_some())
1355 .finish_non_exhaustive()
1356 }
1357}
1358
1359type EncryptionChannels = (
1361 tokio::sync::mpsc::Receiver<Bytes>,
1362 tokio::sync::oneshot::Receiver<DataMap>,
1363 tokio::task::JoinHandle<Result<()>>,
1364);
1365
1366fn spawn_file_encryption(path: PathBuf) -> Result<EncryptionChannels> {
1368 let metadata = std::fs::metadata(&path)?;
1369 let data_size = usize::try_from(metadata.len())
1370 .map_err(|e| Error::Encryption(format!("file size exceeds platform usize: {e}")))?;
1371
1372 let (chunk_tx, chunk_rx) = tokio::sync::mpsc::channel(2);
1373 let (datamap_tx, datamap_rx) = tokio::sync::oneshot::channel();
1374
1375 let handle = tokio::task::spawn_blocking(move || {
1376 let file = std::fs::File::open(&path)?;
1377 let mut reader = std::io::BufReader::new(file);
1378
1379 let read_error: Arc<Mutex<Option<std::io::Error>>> = Arc::new(Mutex::new(None));
1380 let read_error_clone = Arc::clone(&read_error);
1381
1382 let data_iter = std::iter::from_fn(move || {
1383 let mut buffer = vec![0u8; 8192];
1384 match std::io::Read::read(&mut reader, &mut buffer) {
1385 Ok(0) => None,
1386 Ok(n) => {
1387 buffer.truncate(n);
1388 Some(Bytes::from(buffer))
1389 }
1390 Err(e) => {
1391 let mut guard = read_error_clone
1392 .lock()
1393 .unwrap_or_else(|poisoned| poisoned.into_inner());
1394 *guard = Some(e);
1395 None
1396 }
1397 }
1398 });
1399
1400 let mut stream = stream_encrypt(data_size, data_iter)
1401 .map_err(|e| Error::Encryption(format!("stream_encrypt failed: {e}")))?;
1402
1403 for chunk_result in stream.chunks() {
1404 {
1409 let guard = read_error
1410 .lock()
1411 .unwrap_or_else(|poisoned| poisoned.into_inner());
1412 if let Some(ref e) = *guard {
1413 return Err(Error::Io(std::io::Error::new(e.kind(), e.to_string())));
1414 }
1415 }
1416
1417 let (_hash, content) = chunk_result
1418 .map_err(|e| Error::Encryption(format!("chunk encryption failed: {e}")))?;
1419 if chunk_tx.blocking_send(content).is_err() {
1420 return Err(Error::Encryption("upload receiver dropped".to_string()));
1421 }
1422 }
1423
1424 {
1426 let guard = read_error
1427 .lock()
1428 .unwrap_or_else(|poisoned| poisoned.into_inner());
1429 if let Some(ref e) = *guard {
1430 return Err(Error::Io(std::io::Error::new(e.kind(), e.to_string())));
1431 }
1432 }
1433
1434 let datamap = stream
1435 .into_datamap()
1436 .ok_or_else(|| Error::Encryption("no DataMap after encryption".to_string()))?;
1437 if datamap_tx.send(datamap).is_err() {
1438 warn!("DataMap receiver dropped — upload may have been cancelled");
1439 }
1440 Ok(())
1441 });
1442
1443 Ok((chunk_rx, datamap_rx, handle))
1444}
1445
1446struct TempDownload {
1453 path: Option<PathBuf>,
1455}
1456
1457impl TempDownload {
1458 fn new(path: PathBuf) -> Self {
1459 Self { path: Some(path) }
1460 }
1461
1462 fn path(&self) -> &Path {
1464 self.path
1465 .as_deref()
1466 .expect("TempDownload::path called after commit")
1467 }
1468
1469 fn commit(mut self, dest: &Path) -> std::io::Result<()> {
1473 std::fs::rename(self.path(), dest)?; self.path = None; Ok(())
1476 }
1477}
1478
1479impl Drop for TempDownload {
1480 fn drop(&mut self) {
1481 if let Some(path) = self.path.take() {
1482 if let Err(e) = std::fs::remove_file(&path) {
1483 if e.kind() != std::io::ErrorKind::NotFound {
1485 warn!(
1486 "Failed to remove temp download file {}: {e}",
1487 path.display()
1488 );
1489 }
1490 }
1491 }
1492 }
1493}
1494
1495struct SpillUploadAdapter<'a> {
1496 client: &'a Client,
1497 spill: &'a ChunkSpill,
1498 progress: Option<&'a mpsc::Sender<UploadEvent>>,
1499 checkpoint: &'a Path,
1500}
1501
1502#[async_trait::async_trait]
1503impl super::super::upload::UploadAdapter for SpillUploadAdapter<'_> {
1504 #[cfg(feature = "native")]
1505 fn initialize_payment_attempt(&self, attempt: &mut super::super::upload_state::PaymentAttempt) {
1506 super::super::native_payment::initialize(attempt);
1507 }
1508 #[cfg(feature = "native")]
1509 async fn submit_payment(
1510 &self,
1511 plans: &[crate::data::client::batch::ChunkPaymentPlan],
1512 state: &mut crate::data::client::upload_state::UploadState,
1513 ) -> Result<super::super::upload::UploadPayment> {
1514 super::super::native_payment::pay(self.client, self, plans, state).await
1515 }
1516 #[cfg(feature = "native")]
1517 async fn reconcile_payment(
1518 &self,
1519 plans: &[crate::data::client::batch::ChunkPaymentPlan],
1520 state: &mut crate::data::client::upload_state::UploadState,
1521 ) -> Result<super::super::upload::UploadPayment> {
1522 super::super::native_payment::pay(self.client, self, plans, state).await
1523 }
1524 #[cfg(feature = "native")]
1525 async fn submit_merkle_payment(
1526 &self,
1527 batch: &super::super::merkle::PreparedMerkleBatch,
1528 state: &mut crate::data::client::upload_state::UploadState,
1529 ) -> Result<super::super::upload::MerkleUploadPayment> {
1530 super::super::native_payment::pay_merkle(self.client, self, batch, state).await
1531 }
1532 #[cfg(feature = "native")]
1533 async fn reconcile_merkle_payment(
1534 &self,
1535 batch: &super::super::merkle::PreparedMerkleBatch,
1536 state: &mut crate::data::client::upload_state::UploadState,
1537 ) -> Result<super::super::upload::MerkleUploadPayment> {
1538 super::super::native_payment::pay_merkle(self.client, self, batch, state).await
1539 }
1540 async fn load(&self, record: super::super::upload::UploadRecord) -> Result<Bytes> {
1541 self.spill.read_chunk(&record.address)
1542 }
1543 async fn pay(
1544 &self,
1545 plans: &[crate::data::client::batch::ChunkPaymentPlan],
1546 ) -> Result<super::super::upload::UploadPayment> {
1547 let adapter = super::super::upload::MemoryUploadAdapter {
1548 client: self.client,
1549 chunks: &[],
1550 progress: self.progress,
1551 stored_offset: 0,
1552 file_total: self.spill.len(),
1553 resume_key: None,
1554 };
1555 adapter.pay(plans).await
1556 }
1557 async fn pay_merkle(
1558 &self,
1559 batch: &PreparedMerkleBatch,
1560 ) -> Result<super::super::upload::MerkleUploadPayment> {
1561 let adapter = super::super::upload::MemoryUploadAdapter {
1562 client: self.client,
1563 chunks: &[],
1564 progress: self.progress,
1565 stored_offset: 0,
1566 file_total: self.spill.len(),
1567 resume_key: None,
1568 };
1569 adapter.pay_merkle(batch).await
1570 }
1571 async fn checkpoint(
1572 &self,
1573 state: &super::super::upload_state::UploadState,
1574 _: Option<&super::super::upload::UploadPayment>,
1575 ) -> Result<()> {
1576 use std::io::Write;
1577 let bytes = state.checkpoint()?;
1578 let directory = self
1579 .checkpoint
1580 .parent()
1581 .ok_or_else(|| Error::Config("missing checkpoint directory".into()))?;
1582 let mut file = tempfile::NamedTempFile::new_in(directory)?;
1583 file.write_all(&bytes)?;
1584 file.as_file().sync_all()?;
1585 file.persist(self.checkpoint)
1586 .map_err(|e| Error::Io(e.error))?;
1587 Ok(())
1588 }
1589 fn stored(&self, stored: usize, total: usize) {
1590 if let Some(progress) = self.progress {
1591 let _ = progress.try_send(UploadEvent::ChunkStored { stored, total });
1592 }
1593 }
1594 fn quoted(&self, quoted: usize, total: usize) {
1595 if let Some(progress) = self.progress {
1596 let _ = progress.try_send(UploadEvent::ChunkQuoted { quoted, total });
1597 }
1598 }
1599}
1600
1601impl Client {
1602 pub async fn file_upload(&self, path: &Path) -> Result<FileUploadResult> {
1613 self.file_upload_with_mode(path, PaymentMode::Auto).await
1614 }
1615
1616 pub async fn estimate_upload_cost(
1647 &self,
1648 path: &Path,
1649 mode: PaymentMode,
1650 progress: Option<mpsc::Sender<UploadEvent>>,
1651 ) -> Result<UploadCostEstimate> {
1652 let file_size = std::fs::metadata(path).map_err(Error::Io)?.len();
1653
1654 if file_size < 3 {
1655 return Err(Error::InvalidData(
1656 "File too small: self-encryption requires at least 3 bytes".into(),
1657 ));
1658 }
1659
1660 check_disk_space_for_spill(file_size)?;
1661
1662 info!(
1663 "Estimating upload cost for {} ({file_size} bytes)",
1664 path.display()
1665 );
1666
1667 let (spill, _data_map) = self.encrypt_file_to_spill(path, progress.as_ref()).await?;
1668 let chunk_count = spill.len();
1669
1670 if let Some(ref tx) = progress {
1671 let _ = tx
1672 .send(UploadEvent::Encrypted {
1673 total_chunks: chunk_count,
1674 })
1675 .await;
1676 }
1677
1678 info!("Encrypted into {chunk_count} chunks, requesting quote");
1679 let uses_merkle = should_use_merkle(chunk_count, mode);
1680
1681 let sample_indices = distributed_sample_indices(spill.addresses.len(), ESTIMATE_SAMPLE_CAP);
1688 let mut sampled = 0usize;
1689 let mut all_already_stored = true;
1690 let mut quotes_opt: Option<Vec<QuoteEntry>> = None;
1691
1692 for &idx in &sample_indices {
1693 let addr = &spill.addresses[idx];
1694 sampled += 1;
1695 let chunk_bytes = spill.read_chunk(addr)?;
1696 let data_size = u64::try_from(chunk_bytes.len())
1697 .map_err(|e| Error::InvalidData(format!("chunk size too large: {e}")))?;
1698 let result = if uses_merkle {
1699 self.get_store_quotes_with_fault_tolerance(addr, data_size, DATA_TYPE_CHUNK)
1700 .await
1701 } else {
1702 self.get_store_quotes(addr, data_size, DATA_TYPE_CHUNK)
1703 .await
1704 };
1705 match result {
1706 Ok(q) => {
1707 quotes_opt = Some(q);
1708 all_already_stored = false;
1709 break;
1710 }
1711 Err(Error::AlreadyStored) => {
1712 debug!(
1713 "Sample chunk {} already stored; trying next address ({sampled}/{})",
1714 hex::encode(addr),
1715 sample_indices.len()
1716 );
1717 continue;
1718 }
1719 Err(e) => return Err(e),
1720 }
1721 }
1722
1723 let quotes = match quotes_opt {
1724 Some(q) => q,
1725 None if all_already_stored && sampled == chunk_count => {
1726 info!("All {chunk_count} chunks already stored; returning zero-cost estimate");
1729 return Ok(UploadCostEstimate {
1730 file_size,
1731 chunk_count,
1732 storage_cost_atto: "0".into(),
1733 estimated_gas_cost_wei: "0".into(),
1734 payment_mode: if uses_merkle {
1735 PaymentMode::Merkle
1736 } else {
1737 PaymentMode::Single
1738 },
1739 confidence: CostEstimateConfidence::VerifiedAllAlreadyStored,
1740 });
1741 }
1742 None => {
1743 info!(
1749 "All {sampled}/{chunk_count} sampled chunks already stored; \
1750 returning incomplete zero-cost estimate"
1751 );
1752 return Ok(UploadCostEstimate {
1753 file_size,
1754 chunk_count,
1755 storage_cost_atto: "0".into(),
1756 estimated_gas_cost_wei: "0".into(),
1757 payment_mode: if uses_merkle {
1758 PaymentMode::Merkle
1759 } else {
1760 PaymentMode::Single
1761 },
1762 confidence: CostEstimateConfidence::AllSamplesAlreadyStoredIncomplete,
1763 });
1764 }
1765 };
1766
1767 let prices: Vec<Amount> = quotes.iter().map(|(_, _, _, price, _)| *price).collect();
1770 let median_price = crate::payment_policy::median_quote_index(&prices)
1771 .map_or(Amount::ZERO, |index| prices[index]);
1772 let per_chunk_cost = crate::payment_policy::enhanced_payment_amount(median_price)
1773 .map_err(|error| Error::Payment(error.to_string()))?;
1774
1775 let chunk_count_u64 = u64::try_from(chunk_count).unwrap_or(u64::MAX);
1776 let billable_units = if uses_merkle {
1782 merkle_billable_leaves(chunk_count_u64)
1783 } else {
1784 chunk_count_u64
1785 };
1786 let total_storage = per_chunk_cost * Amount::from(billable_units);
1787
1788 let waves = u128::try_from(chunk_count.div_ceil(UPLOAD_WAVE_SIZE)).unwrap_or(u128::MAX);
1804 let merkle_batches =
1807 u128::try_from(merkle_batch_sizes(chunk_count).len()).unwrap_or(u128::MAX);
1808 let estimated_gas: u128 = if uses_merkle {
1809 merkle_batches
1810 .saturating_mul(GAS_PER_MERKLE_TX)
1811 .saturating_mul(ARBITRUM_GAS_PRICE_WEI)
1812 } else {
1813 waves
1814 .saturating_mul(GAS_PER_WAVE_TX)
1815 .saturating_mul(ARBITRUM_GAS_PRICE_WEI)
1816 };
1817
1818 info!(
1819 "Estimate: {chunk_count} chunks, storage={total_storage} atto, gas~={estimated_gas} wei"
1820 );
1821
1822 Ok(UploadCostEstimate {
1823 file_size,
1824 chunk_count,
1825 storage_cost_atto: total_storage.to_string(),
1826 estimated_gas_cost_wei: estimated_gas.to_string(),
1827 payment_mode: if uses_merkle {
1828 PaymentMode::Merkle
1829 } else {
1830 PaymentMode::Single
1831 },
1832 confidence: CostEstimateConfidence::PricedSample,
1833 })
1834 }
1835
1836 pub async fn file_prepare_upload(&self, path: &Path) -> Result<PreparedUpload> {
1841 self.file_prepare_upload_with_progress(path, Visibility::Private, None)
1842 .await
1843 }
1844
1845 pub async fn file_prepare_upload_with_visibility(
1850 &self,
1851 path: &Path,
1852 visibility: Visibility,
1853 ) -> Result<PreparedUpload> {
1854 self.file_prepare_upload_with_progress(path, visibility, None)
1855 .await
1856 }
1857
1858 pub async fn file_prepare_upload_with_progress(
1863 &self,
1864 path: &Path,
1865 visibility: Visibility,
1866 progress: Option<mpsc::Sender<UploadEvent>>,
1867 ) -> Result<PreparedUpload> {
1868 self.file_prepare_upload_with_mode(path, visibility, PaymentMode::Auto, progress)
1869 .await
1870 }
1871
1872 pub async fn file_prepare_upload_with_mode(
1913 &self,
1914 path: &Path,
1915 visibility: Visibility,
1916 mode: PaymentMode,
1917 progress: Option<mpsc::Sender<UploadEvent>>,
1918 ) -> Result<PreparedUpload> {
1919 debug!(
1920 "Preparing file upload for external signing (visibility={visibility:?}, mode={mode:?}): {}",
1921 path.display()
1922 );
1923
1924 let file_size = std::fs::metadata(path)?.len();
1925 check_disk_space_for_spill(file_size)?;
1926
1927 let (mut spill, data_map) = self.encrypt_file_to_spill(path, progress.as_ref()).await?;
1928
1929 info!(
1930 "Encrypted {} into {} chunks for external signing (spilled to disk)",
1931 path.display(),
1932 spill.len()
1933 );
1934
1935 let data_map_address = match visibility {
1942 Visibility::Private => None,
1943 Visibility::Public => {
1944 let (address, serialized) =
1945 crate::client_engine::files::public_map_record(&data_map)
1946 .map_err(Error::Serialization)?;
1947 info!(
1948 "Public upload: bundling DataMap chunk ({} bytes) at address {}",
1949 serialized.len(),
1950 hex::encode(address)
1951 );
1952 spill.push(&serialized)?;
1953 Some(address)
1954 }
1955 };
1956
1957 let chunk_count = spill.len();
1958
1959 if let Some(ref tx) = progress {
1960 let _ = tx
1961 .send(UploadEvent::Encrypted {
1962 total_chunks: chunk_count,
1963 })
1964 .await;
1965 }
1966
1967 let (payment_info, already_stored_addresses) = if should_use_merkle(chunk_count, mode) {
1968 info!("Using merkle batch preparation for {chunk_count} file chunks");
1971
1972 let chunk_entries = spill.chunk_entries()?;
1973
1974 let merkle_plan = self
1975 .plan_merkle_upload(chunk_entries, DATA_TYPE_CHUNK, progress.as_ref())
1976 .await?;
1977
1978 if merkle_plan.to_upload.is_empty() {
1979 info!("All {chunk_count} file chunks already stored; no external payment needed");
1980 (
1981 ExternalPaymentInfo::WaveBatch {
1982 prepared_chunks: Vec::new(),
1983 payment_intent: PaymentIntent::from_prepared_chunks(&[]),
1984 },
1985 merkle_plan.already_stored,
1986 )
1987 } else if !should_use_merkle(merkle_plan.to_upload.len(), mode) {
1988 info!(
1989 "{} file chunks need upload after merkle preflight; preparing wave-batch payment",
1990 merkle_plan.to_upload.len()
1991 );
1992 let chunk_data = spill.read_chunks(&merkle_plan.to_upload)?;
1993 let (payment_info, mut wave_already_stored) = self
1994 .prepare_wave_batch_external_chunks(chunk_data, progress.as_ref(), chunk_count)
1995 .await?;
1996 let mut already_stored = merkle_plan.already_stored;
1997 already_stored.append(&mut wave_already_stored);
1998 (payment_info, already_stored)
1999 } else {
2000 match self
2005 .prepare_merkle_batches_external(
2006 &merkle_plan.to_upload,
2007 DATA_TYPE_CHUNK,
2008 merkle_plan.to_upload_avg_size(),
2009 self.merkle_external_batch_cap(),
2010 )
2011 .await
2012 {
2013 Ok(prepared_batches) => {
2014 info!(
2015 "File prepared for external merkle signing: {} chunks in {} sub-batch(es) ({})",
2016 merkle_plan.to_upload.len(),
2017 prepared_batches.len(),
2018 path.display()
2019 );
2020
2021 (
2022 ExternalPaymentInfo::Merkle {
2023 prepared_batches,
2024 chunk_store: ExternalChunkStore::from_spill(spill),
2025 chunk_addresses: merkle_plan.to_upload,
2026 },
2027 merkle_plan.already_stored,
2028 )
2029 }
2030 Err(Error::InsufficientPeers(ref msg)) => {
2031 info!(
2032 "External merkle preparation needs more peers ({msg}); preparing wave-batch payment"
2033 );
2034 let chunk_data = spill.read_chunks(&merkle_plan.to_upload)?;
2035 let (payment_info, mut wave_already_stored) = self
2036 .prepare_wave_batch_external_chunks(
2037 chunk_data,
2038 progress.as_ref(),
2039 chunk_count,
2040 )
2041 .await?;
2042 let mut already_stored = merkle_plan.already_stored;
2043 already_stored.append(&mut wave_already_stored);
2044 (payment_info, already_stored)
2045 }
2046 Err(e) => return Err(e),
2047 }
2048 }
2049 } else {
2050 let chunk_data = spill.read_all_chunks()?;
2053 self.prepare_wave_batch_external_chunks(chunk_data, progress.as_ref(), chunk_count)
2054 .await?
2055 };
2056
2057 if let Some(addr) = data_map_address {
2063 let data_map_needs_payment = match &payment_info {
2064 ExternalPaymentInfo::WaveBatch {
2065 prepared_chunks, ..
2066 } => prepared_chunks.iter().any(|c| c.address == addr),
2067 ExternalPaymentInfo::Merkle {
2068 chunk_addresses, ..
2069 } => chunk_addresses.contains(&addr),
2070 };
2071 if !data_map_needs_payment {
2072 info!(
2073 "Public upload: DataMap chunk {} was already stored \
2074 on the network — address is retrievable without a \
2075 new payment",
2076 hex::encode(addr)
2077 );
2078 }
2079 }
2080
2081 Ok(PreparedUpload {
2082 data_map,
2083 payment_info,
2084 data_map_address,
2085 already_stored_addresses,
2086 total_chunks: chunk_count,
2087 })
2088 }
2089
2090 async fn prepare_wave_batch_external_chunks(
2091 &self,
2092 chunk_data: Vec<Bytes>,
2093 progress: Option<&mpsc::Sender<UploadEvent>>,
2094 progress_total: usize,
2095 ) -> Result<(ExternalPaymentInfo, Vec<[u8; 32]>)> {
2096 let chunk_count = chunk_data.len();
2097 let chunks_with_addr: Vec<(Bytes, [u8; 32])> = chunk_data
2098 .into_iter()
2099 .map(|content| {
2100 let address = compute_address(&content);
2101 (content, address)
2102 })
2103 .collect();
2104
2105 let quote_limiter = self.controller().quote.clone();
2109 let quote_concurrency = quote_limiter.current().min(chunk_count.max(1));
2110 let mut quote_stream = crate::client_engine::bounded_unordered(
2111 chunks_with_addr.into_iter().map(|(content, address)| {
2112 let limiter = quote_limiter.clone();
2113 async move {
2114 let result = observe_op(
2115 &limiter,
2116 || async move { self.prepare_chunk_payment(content).await },
2117 classify_error,
2118 )
2119 .await;
2120 (address, result)
2121 }
2122 }),
2123 quote_concurrency,
2124 );
2125
2126 let mut prepared_chunks = Vec::with_capacity(chunk_count);
2127 let mut already_stored = Vec::new();
2128 let mut quoted = 0usize;
2129 while let Some((address, result)) = quote_stream.next().await {
2130 match result? {
2131 Some(prepared) => prepared_chunks.push(prepared),
2132 None => already_stored.push(address),
2133 }
2134 quoted += 1;
2135 if let Some(tx) = progress {
2136 let _ = tx.try_send(UploadEvent::ChunkQuoted {
2137 quoted,
2138 total: progress_total,
2139 });
2140 }
2141 }
2142
2143 let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);
2144 info!(
2145 "Prepared external wave-batch payment: {} chunks, {} already stored, total {} atto",
2146 prepared_chunks.len(),
2147 already_stored.len(),
2148 payment_intent.total_amount,
2149 );
2150
2151 Ok((
2152 ExternalPaymentInfo::WaveBatch {
2153 prepared_chunks,
2154 payment_intent,
2155 },
2156 already_stored,
2157 ))
2158 }
2159
2160 pub async fn finalize_upload(
2172 &self,
2173 prepared: PreparedUpload,
2174 tx_hash_map: &HashMap<QuoteHash, TxHash>,
2175 ) -> Result<FileUploadResult> {
2176 self.finalize_upload_with_progress(prepared, tx_hash_map, None)
2177 .await
2178 }
2179
2180 pub async fn finalize_upload_with_progress(
2189 &self,
2190 prepared: PreparedUpload,
2191 tx_hash_map: &HashMap<QuoteHash, TxHash>,
2192 progress: Option<mpsc::Sender<UploadEvent>>,
2193 ) -> Result<FileUploadResult> {
2194 let data_map_address = prepared.data_map_address;
2195 let already_stored_addresses = prepared.already_stored_addresses;
2196 let already_stored_count = already_stored_addresses.len();
2197 let total_chunks = prepared.total_chunks;
2198 match prepared.payment_info {
2199 ExternalPaymentInfo::WaveBatch {
2200 prepared_chunks,
2201 payment_intent,
2202 } => {
2203 let paid_chunks = finalize_batch_payment(prepared_chunks, tx_hash_map)?;
2204 let wave_result = self
2205 .store_paid_chunks_with_events(
2206 paid_chunks,
2207 progress.as_ref(),
2208 already_stored_count,
2209 total_chunks,
2210 )
2211 .await;
2212 if !wave_result.failed.is_empty() {
2213 let failed_count = wave_result.failed.len();
2214 let stored_count = already_stored_count + wave_result.stored.len();
2215 let mut stored = already_stored_addresses;
2216 stored.extend(wave_result.stored);
2217 return Err(Error::PartialUpload {
2218 stored,
2219 stored_count,
2220 failed: wave_result.failed,
2221 failed_count,
2222 total_chunks,
2223 spend: Box::new(PartialUploadSpend {
2227 storage_cost_atto: payment_intent.total_amount.to_string(),
2228 gas_cost_wei: 0,
2229 }),
2230 reason: "finalize_upload: chunk storage failed after retries".into(),
2231 });
2232 }
2233 let chunks_stored = already_stored_count + wave_result.stored.len();
2234
2235 info!("External-signer upload finalized: {chunks_stored} chunks stored");
2236
2237 let mut stats = WaveAggregateStats::default();
2238 stats.absorb(&wave_result);
2239
2240 Ok(FileUploadResult {
2241 data_map: prepared.data_map,
2242 chunks_stored,
2243 chunks_failed: 0,
2244 total_chunks,
2245 payment_mode_used: PaymentMode::Single,
2246 storage_cost_atto: payment_intent.total_amount.to_string(),
2249 gas_cost_wei: 0,
2250 data_map_address,
2251 chunk_attempts_total: stats.chunk_attempts_total,
2252 store_durations_ms: stats.store_durations_ms,
2253 retries_histogram: stats.retries_histogram,
2254 })
2255 }
2256 ExternalPaymentInfo::Merkle { .. } => Err(Error::Payment(
2257 "Cannot finalize merkle upload with wave-batch tx hashes. \
2258 Use finalize_upload_merkle() instead."
2259 .to_string(),
2260 )),
2261 }
2262 }
2263
2264 fn merkle_external_batch_cap(&self) -> usize {
2269 self.config()
2270 .merkle_external_batch_cap
2271 .map_or(MAX_LEAVES, |cap| cap.clamp(3, MAX_LEAVES))
2272 }
2273
2274 pub async fn finalize_upload_merkle(
2292 &self,
2293 prepared: PreparedUpload,
2294 winner_pool_hash: [u8; 32],
2295 ) -> Result<FileUploadResult> {
2296 self.finalize_upload_merkle_with_progress(prepared, winner_pool_hash, None)
2297 .await
2298 }
2299
2300 pub async fn finalize_upload_merkle_with_progress(
2309 &self,
2310 prepared: PreparedUpload,
2311 winner_pool_hash: [u8; 32],
2312 progress: Option<mpsc::Sender<UploadEvent>>,
2313 ) -> Result<FileUploadResult> {
2314 if let ExternalPaymentInfo::Merkle {
2315 prepared_batches, ..
2316 } = &prepared.payment_info
2317 {
2318 let batches = prepared_batches.len();
2319 if batches != 1 {
2320 return Err(Error::Payment(format!(
2321 "This upload was prepared as {batches} merkle sub-batches; \
2322 pay each and call finalize_upload_merkle_multi() with one \
2323 winner hash per batch."
2324 )));
2325 }
2326 }
2327 self.finalize_upload_merkle_multi_with_progress(
2328 prepared,
2329 vec![Some(winner_pool_hash)],
2330 progress,
2331 )
2332 .await
2333 }
2334
2335 pub async fn finalize_upload_merkle_multi(
2357 &self,
2358 prepared: PreparedUpload,
2359 winner_pool_hashes: Vec<Option<[u8; 32]>>,
2360 ) -> Result<FileUploadResult> {
2361 self.finalize_upload_merkle_multi_with_progress(prepared, winner_pool_hashes, None)
2362 .await
2363 }
2364
2365 pub async fn finalize_upload_merkle_multi_with_progress(
2373 &self,
2374 prepared: PreparedUpload,
2375 winner_pool_hashes: Vec<Option<[u8; 32]>>,
2376 progress: Option<mpsc::Sender<UploadEvent>>,
2377 ) -> Result<FileUploadResult> {
2378 let data_map_address = prepared.data_map_address;
2379 let already_stored_addresses = prepared.already_stored_addresses;
2380 let total_chunks = prepared.total_chunks;
2381 match prepared.payment_info {
2382 ExternalPaymentInfo::Merkle {
2383 prepared_batches,
2384 chunk_store,
2385 chunk_addresses,
2386 } => {
2387 let batch_result =
2388 fold_external_merkle_payments(prepared_batches, winner_pool_hashes)?;
2389
2390 let (chunks_stored, _storage_cost, _gas_cost, stats) = self
2391 .upload_merkle_from_spill(
2392 chunk_store.spill(),
2393 &chunk_addresses,
2394 &batch_result,
2395 &already_stored_addresses,
2396 progress.as_ref(),
2397 None,
2402 )
2403 .await?;
2404
2405 info!("External-signer merkle upload finalized: {chunks_stored} chunks stored");
2406
2407 Ok(FileUploadResult {
2408 data_map: prepared.data_map,
2409 chunks_stored,
2410 chunks_failed: 0,
2411 total_chunks,
2412 payment_mode_used: PaymentMode::Merkle,
2413 storage_cost_atto: "0".into(),
2416 gas_cost_wei: 0,
2417 data_map_address,
2418 chunk_attempts_total: stats.chunk_attempts_total,
2419 store_durations_ms: stats.store_durations_ms,
2420 retries_histogram: stats.retries_histogram,
2421 })
2422 }
2423 ExternalPaymentInfo::WaveBatch { .. } => Err(Error::Payment(
2424 "Cannot finalize wave-batch upload with merkle winner hashes. \
2425 Use finalize_upload() instead."
2426 .to_string(),
2427 )),
2428 }
2429 }
2430
2431 pub async fn finalize_upload_merkle_multi_resumable(
2458 &self,
2459 prepared: PreparedUpload,
2460 winner_pool_hashes: Vec<Option<[u8; 32]>>,
2461 ) -> Result<FinalizeOutcome> {
2462 self.finalize_upload_merkle_multi_resumable_with_progress(
2463 prepared,
2464 winner_pool_hashes,
2465 None,
2466 )
2467 .await
2468 }
2469
2470 pub async fn finalize_upload_merkle_multi_resumable_with_progress(
2478 &self,
2479 prepared: PreparedUpload,
2480 winner_pool_hashes: Vec<Option<[u8; 32]>>,
2481 progress: Option<mpsc::Sender<UploadEvent>>,
2482 ) -> Result<FinalizeOutcome> {
2483 let data_map_address = prepared.data_map_address;
2484 let already_stored_addresses = prepared.already_stored_addresses;
2485 let total_chunks = prepared.total_chunks;
2486 let data_map = prepared.data_map;
2487 match prepared.payment_info {
2488 ExternalPaymentInfo::Merkle {
2489 prepared_batches,
2490 chunk_store,
2491 chunk_addresses,
2492 } => {
2493 require_fully_paid_for_resumable(&winner_pool_hashes)?;
2494 let batch_result =
2495 fold_external_merkle_payments(prepared_batches, winner_pool_hashes)?;
2496 self.drive_merkle_finalize(
2497 data_map,
2498 data_map_address,
2499 total_chunks,
2500 chunk_store,
2501 chunk_addresses,
2502 batch_result,
2503 already_stored_addresses,
2504 progress.as_ref(),
2505 )
2506 .await
2507 }
2508 ExternalPaymentInfo::WaveBatch { .. } => Err(Error::Payment(
2509 "Cannot finalize wave-batch upload with merkle winner hashes. \
2510 Use finalize_upload_resumable() instead."
2511 .to_string(),
2512 )),
2513 }
2514 }
2515
2516 pub async fn finalize_upload_resumable(
2534 &self,
2535 prepared: PreparedUpload,
2536 tx_hash_map: &HashMap<QuoteHash, TxHash>,
2537 ) -> Result<FinalizeOutcome> {
2538 self.finalize_upload_resumable_with_progress(prepared, tx_hash_map, None)
2539 .await
2540 }
2541
2542 pub async fn finalize_upload_resumable_with_progress(
2550 &self,
2551 prepared: PreparedUpload,
2552 tx_hash_map: &HashMap<QuoteHash, TxHash>,
2553 progress: Option<mpsc::Sender<UploadEvent>>,
2554 ) -> Result<FinalizeOutcome> {
2555 let data_map_address = prepared.data_map_address;
2556 let already_stored_count = prepared.already_stored_addresses.len();
2557 let total_chunks = prepared.total_chunks;
2558 let data_map = prepared.data_map;
2559 match prepared.payment_info {
2560 ExternalPaymentInfo::WaveBatch {
2561 prepared_chunks,
2562 payment_intent,
2563 } => {
2564 let paid_chunks = finalize_batch_payment(prepared_chunks, tx_hash_map)?;
2565 let storage_cost_atto = payment_intent.total_amount.to_string();
2566 Ok(self
2567 .drive_wave_finalize(
2568 data_map,
2569 data_map_address,
2570 total_chunks,
2571 already_stored_count,
2572 paid_chunks,
2573 storage_cost_atto,
2574 progress.as_ref(),
2575 )
2576 .await)
2577 }
2578 ExternalPaymentInfo::Merkle { .. } => Err(Error::Payment(
2579 "Cannot finalize merkle upload with wave-batch tx hashes. \
2580 Use finalize_upload_merkle_multi_resumable() instead."
2581 .to_string(),
2582 )),
2583 }
2584 }
2585
2586 pub async fn finalize_resume(&self, resume: FinalizeResume) -> Result<FinalizeOutcome> {
2613 self.finalize_resume_with_progress(resume, None).await
2614 }
2615
2616 pub async fn finalize_resume_with_progress(
2623 &self,
2624 resume: FinalizeResume,
2625 progress: Option<mpsc::Sender<UploadEvent>>,
2626 ) -> Result<FinalizeOutcome> {
2627 match resume {
2628 FinalizeResume::Wave(w) => {
2629 let WaveFinalizeResume {
2630 data_map,
2631 data_map_address,
2632 total_chunks,
2633 stored_count,
2634 failed_paid_chunks,
2635 storage_cost_atto,
2636 } = *w;
2637 Ok(self
2638 .drive_wave_finalize(
2639 data_map,
2640 data_map_address,
2641 total_chunks,
2642 stored_count,
2643 failed_paid_chunks,
2644 storage_cost_atto,
2645 progress.as_ref(),
2646 )
2647 .await)
2648 }
2649 FinalizeResume::Merkle(m) => {
2650 let MerkleFinalizeResume {
2651 data_map,
2652 data_map_address,
2653 total_chunks,
2654 chunk_store,
2655 unstored_addresses,
2656 batch_result,
2657 stored_addresses,
2658 } = *m;
2659 self.drive_merkle_finalize(
2660 data_map,
2661 data_map_address,
2662 total_chunks,
2663 chunk_store,
2664 unstored_addresses,
2665 batch_result,
2666 stored_addresses,
2667 progress.as_ref(),
2668 )
2669 .await
2670 }
2671 }
2672 }
2673
2674 #[allow(clippy::too_many_arguments)]
2686 async fn drive_merkle_finalize(
2687 &self,
2688 data_map: DataMap,
2689 data_map_address: Option<[u8; 32]>,
2690 total_chunks: usize,
2691 chunk_store: ExternalChunkStore,
2692 to_store: Vec<[u8; 32]>,
2693 batch_result: MerkleBatchPaymentResult,
2694 stored_addresses: Vec<[u8; 32]>,
2695 progress: Option<&mpsc::Sender<UploadEvent>>,
2696 ) -> Result<FinalizeOutcome> {
2697 let store_result = self
2698 .upload_merkle_from_spill(
2699 chunk_store.spill(),
2700 &to_store,
2701 &batch_result,
2702 &stored_addresses,
2703 progress,
2704 None,
2706 )
2707 .await;
2708 assemble_merkle_finalize_outcome(
2709 store_result,
2710 data_map,
2711 data_map_address,
2712 total_chunks,
2713 chunk_store,
2714 batch_result,
2715 )
2716 }
2717
2718 #[allow(clippy::too_many_arguments)]
2727 async fn drive_wave_finalize(
2728 &self,
2729 data_map: DataMap,
2730 data_map_address: Option<[u8; 32]>,
2731 total_chunks: usize,
2732 already_stored_count: usize,
2733 paid_chunks: Vec<PaidChunk>,
2734 storage_cost_atto: String,
2735 progress: Option<&mpsc::Sender<UploadEvent>>,
2736 ) -> FinalizeOutcome {
2737 let retained: HashMap<[u8; 32], PaidChunk> =
2740 paid_chunks.iter().map(|c| (c.address, c.clone())).collect();
2741 let wave_result = self
2742 .store_paid_chunks_with_events(
2743 paid_chunks,
2744 progress,
2745 already_stored_count,
2746 total_chunks,
2747 )
2748 .await;
2749 assemble_wave_finalize_outcome(
2750 wave_result,
2751 retained,
2752 data_map,
2753 data_map_address,
2754 total_chunks,
2755 already_stored_count,
2756 storage_cost_atto,
2757 )
2758 }
2759
2760 #[allow(clippy::too_many_lines)]
2774 pub async fn file_upload_with_mode(
2775 &self,
2776 path: &Path,
2777 mode: PaymentMode,
2778 ) -> Result<FileUploadResult> {
2779 self.file_upload_with_progress(path, mode, None).await
2780 }
2781
2782 #[allow(clippy::too_many_lines)]
2788 pub async fn file_upload_public_with_mode(
2789 &self,
2790 path: &Path,
2791 mode: PaymentMode,
2792 ) -> Result<FileUploadResult> {
2793 self.file_upload_with_visibility_and_progress(path, mode, Visibility::Public, None)
2794 .await
2795 }
2796
2797 #[allow(clippy::too_many_lines)]
2802 pub async fn file_upload_with_progress(
2803 &self,
2804 path: &Path,
2805 mode: PaymentMode,
2806 progress: Option<mpsc::Sender<UploadEvent>>,
2807 ) -> Result<FileUploadResult> {
2808 self.file_upload_with_visibility_and_progress(path, mode, Visibility::Private, progress)
2809 .await
2810 }
2811
2812 #[allow(clippy::too_many_lines)]
2817 pub async fn file_upload_public_with_progress(
2818 &self,
2819 path: &Path,
2820 mode: PaymentMode,
2821 progress: Option<mpsc::Sender<UploadEvent>>,
2822 ) -> Result<FileUploadResult> {
2823 self.file_upload_with_visibility_and_progress(path, mode, Visibility::Public, progress)
2824 .await
2825 }
2826
2827 #[allow(clippy::too_many_lines)]
2828 async fn file_upload_with_visibility_and_progress(
2829 &self,
2830 path: &Path,
2831 mode: PaymentMode,
2832 visibility: Visibility,
2833 progress: Option<mpsc::Sender<UploadEvent>>,
2834 ) -> Result<FileUploadResult> {
2835 debug!(
2836 "Streaming file upload with mode {mode:?}, visibility {visibility:?}: {}",
2837 path.display()
2838 );
2839
2840 let file_size = std::fs::metadata(path)?.len();
2842 check_disk_space_for_spill(file_size)?;
2843
2844 let (mut spill, data_map) = self.encrypt_file_to_spill(path, progress.as_ref()).await?;
2847
2848 let data_map_address = match visibility {
2849 Visibility::Private => None,
2850 Visibility::Public => {
2851 let (address, serialized) =
2852 crate::client_engine::files::public_map_record(&data_map)
2853 .map_err(Error::Serialization)?;
2854 info!(
2855 "Public upload: adding DataMap chunk ({} bytes) at address {} to payment batch",
2856 serialized.len(),
2857 hex::encode(address)
2858 );
2859 spill.push(&serialized)?;
2860 Some(address)
2861 }
2862 };
2863
2864 let chunk_count = spill.len();
2865 info!(
2866 "Encrypted {} into {chunk_count} chunks (spilled to disk)",
2867 path.display()
2868 );
2869 if let Some(ref tx) = progress {
2870 let _ = tx
2871 .send(UploadEvent::Encrypted {
2872 total_chunks: chunk_count,
2873 })
2874 .await;
2875 }
2876
2877 let file_path_key = std::fs::canonicalize(path)
2878 .map(|p| p.display().to_string())
2879 .unwrap_or_else(|_| path.display().to_string());
2880 let records = spill
2881 .chunk_entries()?
2882 .into_iter()
2883 .enumerate()
2884 .map(
2885 |(index, (address, size))| super::super::upload::UploadRecord {
2886 address,
2887 size,
2888 index,
2889 },
2890 )
2891 .collect::<Vec<_>>();
2892 let wallet = self.require_wallet()?;
2893 let scope = rmp_serde::to_vec(&(
2894 wallet.network(),
2895 records
2896 .iter()
2897 .map(|r| (r.address, r.size))
2898 .collect::<Vec<_>>(),
2899 ))
2900 .map_err(|e| Error::Serialization(e.to_string()))?;
2901 let cache_dir = crate::config::data_dir()
2902 .map_err(|e| Error::Config(e.to_string()))?
2903 .join("payments/upload");
2904 std::fs::create_dir_all(&cache_dir)?;
2905 let cache_path = cache_dir.join(format!(
2906 "{}.msgpack",
2907 hex::encode(blake3::hash(&scope).as_bytes())
2908 ));
2909 let lock_path = cache_path.with_extension("lock");
2910 let _lock = tokio::task::spawn_blocking(move || -> std::io::Result<std::fs::File> {
2911 let file = std::fs::OpenOptions::new()
2912 .create(true)
2913 .truncate(false)
2914 .read(true)
2915 .write(true)
2916 .open(lock_path)?;
2917 fs2::FileExt::lock_exclusive(&file)?;
2918 Ok(file)
2919 })
2920 .await
2921 .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))??;
2922 let mut state = match std::fs::read(&cache_path) {
2923 Ok(bytes) => super::super::upload_state::UploadState::restore(&bytes)?,
2924 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2925 let mut proofs =
2926 crate::data::client::cached_single::try_load_for_file(&file_path_key)
2927 .map(|(_, receipt)| receipt.proofs)
2928 .unwrap_or_default();
2929 if let Some((_, receipt)) =
2930 crate::data::client::cached_merkle::try_load_for_file(&file_path_key)
2931 {
2932 proofs.extend(receipt.proofs);
2933 }
2934 super::super::upload_state::UploadState::from_proofs(proofs)
2935 }
2936 Err(error) => return Err(Error::Io(error)),
2937 };
2938 let adapter = SpillUploadAdapter {
2939 client: self,
2940 spill: &spill,
2941 progress: progress.as_ref(),
2942 checkpoint: &cache_path,
2943 };
2944 let result = self
2945 .upload_records(records, &mut state, &adapter, mode)
2946 .await?;
2947 std::fs::remove_file(&cache_path).or_else(|error| {
2948 if error.kind() == std::io::ErrorKind::NotFound {
2949 Ok(())
2950 } else {
2951 Err(error)
2952 }
2953 })?;
2954 crate::data::client::cached_single::try_delete_for_file(&file_path_key);
2955 crate::data::client::cached_merkle::try_delete_for_file(&file_path_key);
2956 Ok(FileUploadResult {
2957 data_map,
2958 chunks_stored: result.addresses.len(),
2959 chunks_failed: 0,
2960 total_chunks: chunk_count,
2961 payment_mode_used: result.mode,
2962 storage_cost_atto: result.amount.to_string(),
2963 gas_cost_wei: result.gas,
2964 data_map_address,
2965 chunk_attempts_total: result.stats.chunk_attempts_total,
2966 store_durations_ms: result.stats.store_durations_ms,
2967 retries_histogram: result.stats.retries_histogram,
2968 })
2969 }
2970
2971 async fn encrypt_file_to_spill(
2978 &self,
2979 path: &Path,
2980 progress: Option<&mpsc::Sender<UploadEvent>>,
2981 ) -> Result<(ChunkSpill, DataMap)> {
2982 let (mut chunk_rx, datamap_rx, handle) = spawn_file_encryption(path.to_path_buf())?;
2983
2984 let mut spill = ChunkSpill::new()?;
2985 while let Some(content) = chunk_rx.recv().await {
2986 spill.push(&content)?;
2987 let chunks_done = spill.len();
2988 if let Some(tx) = progress {
2989 if chunks_done.is_multiple_of(10) {
2990 let _ = tx.send(UploadEvent::Encrypting { chunks_done }).await;
2991 }
2992 }
2993 if chunks_done % 100 == 0 {
2994 let mb = spill.total_bytes() / (1024 * 1024);
2995 info!(
2996 "Encryption progress: {chunks_done} chunks spilled ({mb} MB) — {}",
2997 path.display()
2998 );
2999 }
3000 }
3001
3002 handle
3004 .await
3005 .map_err(|e| Error::Encryption(format!("encryption task panicked: {e}")))?
3006 .map_err(|e| Error::Encryption(format!("encryption failed: {e}")))?;
3007
3008 let data_map = datamap_rx
3009 .await
3010 .map_err(|_| Error::Encryption("no DataMap from encryption thread".to_string()))?;
3011
3012 Ok((spill, data_map))
3013 }
3014
3015 pub async fn file_download(&self, data_map: &DataMap, output: &Path) -> Result<u64> {
3022 self.file_download_with_progress(data_map, output, None)
3023 .await
3024 }
3025
3026 pub async fn file_download_from_closest_peers(
3036 &self,
3037 data_map: &DataMap,
3038 output: &Path,
3039 peer_count: NonZeroUsize,
3040 ) -> Result<u64> {
3041 self.file_download_with_progress_from_closest_peers(data_map, output, None, peer_count)
3042 .await
3043 }
3044
3045 pub async fn file_download_with_progress_from_closest_peers(
3056 &self,
3057 data_map: &DataMap,
3058 output: &Path,
3059 progress: Option<mpsc::Sender<DownloadEvent>>,
3060 peer_count: NonZeroUsize,
3061 ) -> Result<u64> {
3062 self.file_download_with_progress_using_peer_count(
3063 data_map,
3064 output,
3065 progress,
3066 peer_count.get(),
3067 None,
3068 )
3069 .await
3070 }
3071
3072 pub async fn file_download_with_progress_and_diagnostics_from_closest_peers(
3076 &self,
3077 data_map: &DataMap,
3078 output: &Path,
3079 progress: Option<mpsc::Sender<DownloadEvent>>,
3080 peer_count: NonZeroUsize,
3081 diagnostics: Option<DownloadDiagnosticsSender>,
3082 ) -> Result<u64> {
3083 self.file_download_with_progress_using_peer_count(
3084 data_map,
3085 output,
3086 progress,
3087 peer_count.get(),
3088 diagnostics,
3089 )
3090 .await
3091 }
3092
3093 pub async fn file_download_with_peer_report_from_closest_peers(
3106 &self,
3107 data_map: &DataMap,
3108 output: &Path,
3109 progress: Option<mpsc::Sender<DownloadEvent>>,
3110 peer_count: NonZeroUsize,
3111 ) -> Result<FileDownloadWithPeerReport> {
3112 let chunk_reports = Arc::new(Mutex::new(Vec::new()));
3113 let bytes_written = self
3114 .file_download_with_progress_using_peer_count_and_reports(
3115 data_map,
3116 output,
3117 progress,
3118 peer_count.get(),
3119 Some(chunk_reports.clone()),
3120 None,
3121 )
3122 .await?;
3123
3124 let chunk_reports = chunk_reports
3125 .lock()
3126 .map_err(|_| Error::Storage("file chunk peer report lock poisoned".to_string()))?
3127 .clone();
3128 let chunk_reports = file_chunk_reports_from_recorded_sweeps(chunk_reports);
3129
3130 Ok(FileDownloadWithPeerReport {
3131 bytes_written,
3132 chunk_reports,
3133 })
3134 }
3135
3136 async fn download_fetch_file_chunk(
3137 &self,
3138 idx: usize,
3139 hash: XorName,
3140 context: FileDownloadFetchContext,
3141 is_deferred_retry: bool,
3142 attempt: usize,
3143 ) -> std::result::Result<DownloadBatchEntry, self_encryption::Error> {
3144 let addr = hash.0;
3145 let addr_hex = hex::encode(addr);
3146
3147 let chunk_content = if let Some(peer_reports) = context.peer_reports {
3148 match self
3149 .chunk_get_from_closest_peer_group(&addr, context.peer_count)
3150 .await
3151 {
3152 Ok(results) => {
3153 let (content, sweep) = file_chunk_sweep_report_from_peer_results(
3154 attempt,
3155 is_deferred_retry,
3156 &results,
3157 );
3158 peer_reports
3159 .lock()
3160 .map_err(|_| {
3161 self_encryption::Error::Generic(
3162 "file chunk peer report lock poisoned".to_string(),
3163 )
3164 })?
3165 .push(RecordedFileChunkPeerSweep {
3166 index: idx + 1,
3167 address: addr,
3168 sweep,
3169 });
3170 content
3171 }
3172 Err(e) => {
3173 if is_deferred_retry {
3174 info!(
3175 "Deferred all-peer retry for {addr_hex} hit transient error: {e}; re-deferring"
3176 );
3177 } else {
3178 info!("First-pass all-peer fetch error for {addr_hex}: {e}; deferring");
3179 }
3180 peer_reports
3181 .lock()
3182 .map_err(|_| {
3183 self_encryption::Error::Generic(
3184 "file chunk peer report lock poisoned".to_string(),
3185 )
3186 })?
3187 .push(RecordedFileChunkPeerSweep {
3188 index: idx + 1,
3189 address: addr,
3190 sweep: file_chunk_sweep_report_from_error(
3191 attempt,
3192 is_deferred_retry,
3193 &e,
3194 ),
3195 });
3196 None
3197 }
3198 }
3199 } else {
3200 let diag = context.diagnostics.as_ref().map(|sender| {
3205 ChunkFetchDiagnostics::new(
3206 sender,
3207 attempt,
3208 idx + 1,
3209 addr,
3210 self.controller().fetch.current(),
3211 )
3212 });
3213 match self
3214 .chunk_get_observed_from_closest_peers(&addr, context.peer_count, diag.as_ref())
3215 .await
3216 {
3217 Ok(Some(chunk)) => Some(chunk.content),
3218 Ok(None) => None,
3219 Err(e) => {
3220 if is_deferred_retry {
3221 info!(
3222 "Deferred retry for {addr_hex} hit transient error: {e}; re-deferring"
3223 );
3224 } else {
3225 info!("First-pass fetch error for {addr_hex}: {e}; deferring");
3226 }
3227 None
3228 }
3229 }
3230 };
3231
3232 let Some(content) = chunk_content else {
3233 return Ok((idx, Err(hash)));
3234 };
3235
3236 let fetched = context
3237 .fetched_ref
3238 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3239 + 1;
3240 if is_deferred_retry {
3241 info!(
3242 "Downloaded {fetched}/{} (deferred retry)",
3243 context.total_chunks
3244 );
3245 } else {
3246 let total_chunks = context.total_chunks;
3247 info!("Downloaded {fetched}/{total_chunks}");
3248 }
3249 if let Some(ref tx) = context.progress_ref {
3250 let _ = tx.try_send(DownloadEvent::ChunksFetched {
3251 fetched,
3252 total: context.total_chunks,
3253 });
3254 }
3255
3256 Ok((idx, Ok(content)))
3257 }
3258
3259 async fn download_decrypted_chunks<F, Fut>(
3276 &self,
3277 data_map: &DataMap,
3278 progress: Option<mpsc::Sender<DownloadEvent>>,
3279 peer_count: usize,
3280 peer_reports: Option<Arc<Mutex<Vec<RecordedFileChunkPeerSweep>>>>,
3281 diagnostics: Option<DownloadDiagnosticsSender>,
3282 mut on_chunk: F,
3283 ) -> Result<u64>
3284 where
3285 F: FnMut(Bytes) -> Fut,
3286 Fut: std::future::Future<Output = Result<()>>,
3287 {
3288 let handle = Handle::current();
3289
3290 let root_map = if data_map.is_child() {
3293 let dm_chunks = data_map.len();
3294 if let Some(ref tx) = progress {
3295 let _ = tx.try_send(DownloadEvent::ResolvingDataMap {
3296 total_map_chunks: dm_chunks,
3297 });
3298 }
3299
3300 let resolve_counter = std::sync::atomic::AtomicUsize::new(0);
3301 let resolved = crate::client_engine::files::resolve(
3302 data_map,
3303 &|address| {
3304 let resolve_counter = &resolve_counter;
3305 let progress = &progress;
3306 let diagnostics = &diagnostics;
3307 async move {
3308 let diag = diagnostics.as_ref().map(|sender| {
3309 ChunkFetchDiagnostics::new(
3310 sender,
3311 FIRST_DIAGNOSTIC_FETCH_ATTEMPT,
3312 0,
3313 address,
3314 self.controller().fetch.current(),
3315 )
3316 });
3317 let chunk = self
3318 .chunk_get_observed_from_closest_peers(
3319 &address,
3320 peer_count,
3321 diag.as_ref(),
3322 )
3323 .await?
3324 .ok_or_else(|| {
3325 Error::NotFound(format!(
3326 "DataMap chunk not found: {}",
3327 hex::encode(address)
3328 ))
3329 })?;
3330 let fetched =
3331 resolve_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
3332 if let Some(ref tx) = progress {
3333 let _ = tx.try_send(DownloadEvent::MapChunkFetched { fetched });
3334 }
3335 Ok(chunk.content)
3336 }
3337 },
3338 &|| self.controller().fetch.current(),
3339 )
3340 .await
3341 .map_err(super::super::data::map_read_error)?;
3342
3343 info!(
3344 "Resolved hierarchical DataMap: {} data chunks",
3345 resolved.len()
3346 );
3347 resolved
3348 } else {
3349 data_map.clone()
3350 };
3351
3352 let total_chunks = root_map.len();
3354 if let Some(ref tx) = progress {
3355 let _ = tx.try_send(DownloadEvent::DataMapResolved { total_chunks });
3356 }
3357
3358 let fetched_counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3360 let fetched_for_closure = fetched_counter.clone();
3361 let progress_for_closure = progress.clone();
3362 let peer_reports_for_closure = peer_reports.clone();
3363 let diagnostics_for_closure = diagnostics.clone();
3364
3365 let fetch_limiter_outer = self.controller().fetch.clone();
3366 let usable_memory = usable_memory_bytes();
3367 let configured_batch_floor = stream_decrypt_batch_size();
3368 let fetch_cap = fetch_limiter_outer.current();
3369 let decrypt_batch_size = adaptive_stream_decrypt_batch_size(
3370 total_chunks,
3371 fetch_cap,
3372 configured_batch_floor,
3373 usable_memory,
3374 );
3375 info!(
3376 total_chunks,
3377 fetch_cap,
3378 configured_batch_floor,
3379 ?usable_memory,
3380 decrypt_batch_size,
3381 "Selected adaptive stream decrypt batch size"
3382 );
3383
3384 let stream = streaming_decrypt_with_batch_size(
3385 &root_map,
3386 |batch: &[(usize, XorName)]| {
3387 let batch_owned: Vec<(usize, XorName)> = batch.to_vec();
3388 let fetch_context = FileDownloadFetchContext {
3389 total_chunks,
3390 peer_count,
3391 fetched_ref: fetched_for_closure.clone(),
3392 progress_ref: progress_for_closure.clone(),
3393 peer_reports: peer_reports_for_closure.clone(),
3394 diagnostics: diagnostics_for_closure.clone(),
3395 };
3396 let fetch_limiter = fetch_limiter_outer.clone();
3397
3398 tokio::task::block_in_place(|| {
3399 handle.block_on(async {
3400 crate::client_engine::files::deferred_batch(
3401 batch_owned,
3402 |idx, hash, attempt| {
3403 self.download_fetch_file_chunk(
3404 idx,
3405 hash,
3406 fetch_context.clone(),
3407 attempt > 1,
3408 attempt,
3409 )
3410 },
3411 || fetch_limiter.current(),
3412 tokio::time::sleep,
3413 |hash: XorName| {
3414 self_encryption::Error::Generic(format!(
3415 "Chunk not found after 3 deferred retry rounds: {}",
3416 hex::encode(hash.0),
3417 ))
3418 },
3419 )
3420 .await
3421 })
3422 })
3423 },
3424 decrypt_batch_size,
3425 )
3426 .map_err(|e| Error::Encryption(format!("streaming decrypt failed: {e}")))?;
3427
3428 let mut bytes_total = 0u64;
3433 for chunk_result in stream {
3434 let chunk: Bytes =
3435 chunk_result.map_err(|e| Error::Encryption(format!("decryption failed: {e}")))?;
3436 bytes_total += chunk.len() as u64;
3437 on_chunk(chunk).await?;
3438 }
3439 Ok(bytes_total)
3440 }
3441
3442 pub async fn file_download_with_progress(
3449 &self,
3450 data_map: &DataMap,
3451 output: &Path,
3452 progress: Option<mpsc::Sender<DownloadEvent>>,
3453 ) -> Result<u64> {
3454 self.file_download_with_progress_using_peer_count(
3455 data_map,
3456 output,
3457 progress,
3458 self.config().close_group_size,
3459 None,
3460 )
3461 .await
3462 }
3463
3464 async fn file_download_with_progress_using_peer_count(
3470 &self,
3471 data_map: &DataMap,
3472 output: &Path,
3473 progress: Option<mpsc::Sender<DownloadEvent>>,
3474 peer_count: usize,
3475 diagnostics: Option<DownloadDiagnosticsSender>,
3476 ) -> Result<u64> {
3477 self.file_download_with_progress_using_peer_count_and_reports(
3478 data_map,
3479 output,
3480 progress,
3481 peer_count,
3482 None,
3483 diagnostics,
3484 )
3485 .await
3486 }
3487
3488 async fn file_download_with_progress_using_peer_count_and_reports(
3489 &self,
3490 data_map: &DataMap,
3491 output: &Path,
3492 progress: Option<mpsc::Sender<DownloadEvent>>,
3493 peer_count: usize,
3494 peer_reports: Option<Arc<Mutex<Vec<RecordedFileChunkPeerSweep>>>>,
3495 diagnostics: Option<DownloadDiagnosticsSender>,
3496 ) -> Result<u64> {
3497 debug!("Downloading file to {}", output.display());
3498
3499 let parent = output.parent().unwrap_or_else(|| Path::new("."));
3500 let unique: u64 = rand::random();
3501 let tmp_path = parent.join(format!(".ant_download_{}_{unique}.tmp", std::process::id()));
3502
3503 let tmp = TempDownload::new(tmp_path);
3508 let mut file = std::fs::File::create(tmp.path())?;
3509
3510 let bytes_written = self
3511 .download_decrypted_chunks(
3512 data_map,
3513 progress,
3514 peer_count,
3515 peer_reports,
3516 diagnostics,
3517 |bytes| {
3518 let r = file.write_all(&bytes).map_err(Error::from);
3519 std::future::ready(r)
3520 },
3521 )
3522 .await?;
3523 file.flush()?;
3524 drop(file); tmp.commit(output)?;
3527 info!(
3528 "File downloaded: {bytes_written} bytes written to {}",
3529 output.display()
3530 );
3531 Ok(bytes_written)
3532 }
3533
3534 pub async fn file_download_to_sender(
3554 &self,
3555 data_map: &DataMap,
3556 sink: mpsc::Sender<std::result::Result<Bytes, Error>>,
3557 progress: Option<mpsc::Sender<DownloadEvent>>,
3558 ) -> Result<u64> {
3559 let peer_count = self.config().close_group_size;
3560 self.download_decrypted_chunks(data_map, progress, peer_count, None, None, |bytes| {
3561 let sink = sink.clone();
3562 async move {
3563 sink.send(Ok(bytes))
3564 .await
3565 .map_err(|_| Error::Cancelled("download stream receiver dropped".into()))
3566 }
3567 })
3568 .await
3569 }
3570}
3571
3572#[cfg(test)]
3573#[allow(clippy::unwrap_used)]
3574mod tests {
3575 use super::*;
3576
3577 fn dummy_batch_result() -> MerkleBatchPaymentResult {
3580 MerkleBatchPaymentResult {
3581 proofs: HashMap::new(),
3582 chunk_count: 0,
3583 storage_cost_atto: "0".into(),
3584 gas_cost_wei: 0,
3585 merkle_payment_timestamp: 0,
3586 }
3587 }
3588
3589 fn empty_chunk_store() -> ExternalChunkStore {
3590 ExternalChunkStore::from_spill(ChunkSpill::new().unwrap())
3591 }
3592
3593 fn paid_chunk(address: [u8; 32]) -> PaidChunk {
3596 PaidChunk {
3597 content: Bytes::from_static(b"x"),
3598 address,
3599 quoted_peers: Vec::new(),
3600 proof_bytes: Vec::new(),
3601 }
3602 }
3603
3604 #[test]
3605 fn assemble_complete_on_full_store() {
3606 let outcome = assemble_merkle_finalize_outcome(
3607 Ok((3, "0".into(), 0, WaveAggregateStats::default())),
3608 DataMap::new(vec![]),
3609 Some([9u8; 32]),
3610 3,
3611 empty_chunk_store(),
3612 dummy_batch_result(),
3613 )
3614 .expect("a fully-stored pass is not an error");
3615 match outcome {
3616 FinalizeOutcome::Complete(result) => {
3617 assert_eq!(result.chunks_stored, 3);
3618 assert_eq!(result.chunks_failed, 0);
3619 assert_eq!(result.total_chunks, 3);
3620 assert_eq!(result.data_map_address, Some([9u8; 32]));
3621 assert!(matches!(result.payment_mode_used, PaymentMode::Merkle));
3622 }
3623 FinalizeOutcome::Partial { .. } => panic!("expected Complete"),
3624 }
3625 }
3626
3627 #[test]
3628 fn assemble_partial_retains_resume_for_unstored() {
3629 let a = [1u8; 32];
3630 let b = [2u8; 32];
3631 let c = [3u8; 32];
3632 let store_result = Err(Error::PartialUpload {
3634 stored: vec![a],
3635 stored_count: 1,
3636 failed: vec![(b, "quorum".into()), (c, "quorum".into())],
3637 failed_count: 2,
3638 total_chunks: 3,
3639 spend: Box::new(PartialUploadSpend {
3640 storage_cost_atto: "777".into(),
3641 gas_cost_wei: 0,
3642 }),
3643 reason: "merkle chunk store aborted".into(),
3644 });
3645 let outcome = assemble_merkle_finalize_outcome(
3646 store_result,
3647 DataMap::new(vec![]),
3648 Some([9u8; 32]),
3649 3,
3650 empty_chunk_store(),
3651 dummy_batch_result(),
3652 )
3653 .expect("a quorum shortfall is Ok(Partial), never Err");
3654 match outcome {
3655 FinalizeOutcome::Partial { result, resume } => {
3656 assert_eq!(result.chunks_stored, 1);
3658 assert_eq!(result.chunks_failed, 2);
3659 assert_eq!(result.total_chunks, 3);
3660 assert_eq!(result.storage_cost_atto, "777");
3661 let FinalizeResume::Merkle(m) = resume else {
3662 panic!("expected a merkle resume handle");
3663 };
3664 assert_eq!(m.unstored_addresses, vec![b, c]);
3667 assert_eq!(m.stored_addresses, vec![a]);
3668 assert_eq!(m.total_chunks, 3);
3669 assert_eq!(m.data_map_address, Some([9u8; 32]));
3670 }
3671 FinalizeOutcome::Complete(_) => panic!("expected Partial"),
3672 }
3673 }
3674
3675 #[test]
3676 fn resumable_guard_rejects_partial_payment() {
3677 let err = require_fully_paid_for_resumable(&[Some([1u8; 32]), None, Some([2u8; 32])])
3682 .expect_err("a mix of paid and unpaid sub-batches must be rejected");
3683 match err {
3684 Error::Payment(msg) => {
3685 assert!(msg.contains("1/3"), "counts unpaid batches: {msg}");
3686 assert!(
3687 msg.contains("finalize_upload_merkle_multi()"),
3688 "points at the non-resumable path: {msg}"
3689 );
3690 }
3691 other => panic!("expected Error::Payment, got {other:?}"),
3692 }
3693 }
3694
3695 #[test]
3696 fn resumable_guard_accepts_fully_paid() {
3697 require_fully_paid_for_resumable(&[Some([1u8; 32]), Some([2u8; 32])])
3698 .expect("fully-paid winner hashes pass the guard");
3699 require_fully_paid_for_resumable(&[]).expect(
3700 "an empty set has no unpaid batch — fold_external_merkle_payments \
3701 rejects it as nothing-to-finalize",
3702 );
3703 }
3704
3705 #[test]
3706 fn merkle_resume_handle_drains_to_complete() {
3707 let a = [1u8; 32];
3713 let b = [2u8; 32];
3714 let c = [3u8; 32];
3715 let first_pass = Err(Error::PartialUpload {
3716 stored: vec![a],
3717 stored_count: 1,
3718 failed: vec![(b, "quorum".into()), (c, "quorum".into())],
3719 failed_count: 2,
3720 total_chunks: 3,
3721 spend: Box::new(PartialUploadSpend {
3722 storage_cost_atto: "777".into(),
3723 gas_cost_wei: 0,
3724 }),
3725 reason: "quorum shortfall".into(),
3726 });
3727 let outcome = assemble_merkle_finalize_outcome(
3728 first_pass,
3729 DataMap::new(vec![]),
3730 Some([9u8; 32]),
3731 3,
3732 empty_chunk_store(),
3733 dummy_batch_result(),
3734 )
3735 .expect("a quorum shortfall is Ok(Partial), never Err");
3736 let FinalizeOutcome::Partial { resume, .. } = outcome else {
3737 panic!("expected Partial after a shortfall pass");
3738 };
3739 let FinalizeResume::Merkle(m) = resume else {
3740 panic!("expected a merkle resume handle");
3741 };
3742 assert_eq!(m.unstored_addresses, vec![b, c]);
3743
3744 let second_pass = Ok((3, "0".into(), 0, WaveAggregateStats::default()));
3747 let outcome = assemble_merkle_finalize_outcome(
3748 second_pass,
3749 m.data_map,
3750 m.data_map_address,
3751 m.total_chunks,
3752 m.chunk_store,
3753 m.batch_result,
3754 )
3755 .expect("a fully-stored resume pass is not an error");
3756 match outcome {
3757 FinalizeOutcome::Complete(result) => {
3758 assert_eq!(result.chunks_stored, 3);
3759 assert_eq!(result.chunks_failed, 0);
3760 assert_eq!(result.total_chunks, 3);
3761 assert_eq!(result.data_map_address, Some([9u8; 32]));
3762 }
3763 FinalizeOutcome::Partial { .. } => panic!("expected Complete after the drain pass"),
3764 }
3765 }
3766
3767 #[test]
3768 fn assemble_propagates_fatal_error() {
3769 let outcome = assemble_merkle_finalize_outcome(
3771 Err(Error::Payment("on-chain call reverted".into())),
3772 DataMap::new(vec![]),
3773 None,
3774 3,
3775 empty_chunk_store(),
3776 dummy_batch_result(),
3777 );
3778 assert!(matches!(outcome, Err(Error::Payment(_))));
3779 }
3780
3781 #[test]
3782 fn assemble_wave_complete_when_all_stored() {
3783 let a = [1u8; 32];
3784 let wave_result = WaveResult {
3785 stored: vec![a],
3786 failed: Vec::new(),
3787 chunk_attempts_total: 1,
3788 store_durations_ms: vec![5],
3789 retries_per_chunk: vec![0],
3790 };
3791 let mut retained = HashMap::new();
3792 retained.insert(a, paid_chunk(a));
3793 let outcome = assemble_wave_finalize_outcome(
3794 wave_result,
3795 retained,
3796 DataMap::new(vec![]),
3797 Some([9u8; 32]),
3798 1,
3799 0,
3800 "500".into(),
3801 );
3802 match outcome {
3803 FinalizeOutcome::Complete(result) => {
3804 assert_eq!(result.chunks_stored, 1);
3805 assert_eq!(result.chunks_failed, 0);
3806 assert_eq!(result.storage_cost_atto, "500");
3807 assert!(matches!(result.payment_mode_used, PaymentMode::Single));
3808 }
3809 FinalizeOutcome::Partial { .. } => panic!("expected Complete"),
3810 }
3811 }
3812
3813 #[test]
3814 fn assemble_wave_partial_retains_failed_paid_chunks() {
3815 let a = [1u8; 32]; let b = [2u8; 32]; let c = [3u8; 32]; let wave_result = WaveResult {
3819 stored: vec![a],
3820 failed: vec![(b, "quorum".into()), (c, "quorum".into())],
3821 chunk_attempts_total: 3,
3822 store_durations_ms: vec![5],
3823 retries_per_chunk: vec![0],
3824 };
3825 let mut retained = HashMap::new();
3827 for addr in [a, b, c] {
3828 retained.insert(addr, paid_chunk(addr));
3829 }
3830 let outcome = assemble_wave_finalize_outcome(
3831 wave_result,
3832 retained,
3833 DataMap::new(vec![]),
3834 Some([9u8; 32]),
3835 3,
3836 0,
3837 "500".into(),
3838 );
3839 match outcome {
3840 FinalizeOutcome::Partial { result, resume } => {
3841 assert_eq!(result.chunks_stored, 1);
3842 assert_eq!(result.chunks_failed, 2);
3843 assert_eq!(result.storage_cost_atto, "500");
3844 let FinalizeResume::Wave(w) = resume else {
3845 panic!("expected a wave resume handle");
3846 };
3847 let mut got: Vec<[u8; 32]> =
3849 w.failed_paid_chunks.iter().map(|pc| pc.address).collect();
3850 got.sort();
3851 assert_eq!(got, vec![b, c]);
3852 assert_eq!(w.stored_count, 1);
3853 assert_eq!(w.total_chunks, 3);
3854 }
3855 FinalizeOutcome::Complete(_) => panic!("expected Partial"),
3856 }
3857 }
3858
3859 #[test]
3860 fn merkle_store_cap_clamps_to_memory_bound() {
3861 assert_eq!(merkle_store_cap(8), 8);
3863 assert_eq!(merkle_store_cap(64), 64);
3864 assert_eq!(merkle_store_cap(512), MERKLE_STORE_MAX_IN_FLIGHT);
3867 assert_eq!(merkle_store_cap(usize::MAX), MERKLE_STORE_MAX_IN_FLIGHT);
3868 assert_eq!(merkle_store_cap(0), 1);
3870 }
3871
3872 #[test]
3873 fn distributed_sample_indices_spreads_across_large_file() {
3874 assert_eq!(distributed_sample_indices(100, 5), vec![0, 24, 49, 74, 99]);
3876 }
3877
3878 #[test]
3879 fn distributed_sample_indices_covers_whole_small_file() {
3880 assert_eq!(distributed_sample_indices(3, 5), vec![0, 1, 2]);
3883 assert_eq!(distributed_sample_indices(5, 5), vec![0, 1, 2, 3, 4]);
3884 }
3885
3886 #[test]
3890 fn estimator_leaf_total_is_the_padded_payment_partition() {
3891 for chunks in [2u64, 64, 65, 100, 129, 255, 256, 257, 300, 512, 513, 769] {
3892 let from_partition: u64 = merkle_batch_sizes(chunks as usize)
3893 .into_iter()
3894 .map(|size| size.next_power_of_two() as u64)
3895 .sum();
3896 assert_eq!(
3897 merkle_billable_leaves(chunks),
3898 from_partition,
3899 "{chunks} chunks must be billed for the partition the payment path pays"
3900 );
3901 }
3902 }
3903
3904 #[test]
3905 fn distributed_sample_indices_is_in_range_and_increasing() {
3906 assert!(distributed_sample_indices(0, 5).is_empty());
3907 assert_eq!(distributed_sample_indices(1, 5), vec![0]);
3908 for total in 1..200usize {
3909 let idx = distributed_sample_indices(total, 5);
3910 assert_eq!(*idx.first().unwrap(), 0);
3911 assert_eq!(*idx.last().unwrap(), total - 1);
3912 assert!(idx.iter().all(|&i| i < total));
3913 assert!(idx.windows(2).all(|w| w[0] < w[1]));
3914 }
3915 }
3916
3917 #[test]
3918 fn disk_space_check_passes_for_small_file() {
3919 check_disk_space_for_spill(1024).unwrap();
3921 }
3922
3923 #[test]
3924 fn disk_space_check_fails_for_absurd_size() {
3925 let result = check_disk_space_for_spill(u64::MAX / 2);
3927 assert!(result.is_err());
3928 let err = result.unwrap_err();
3929 assert!(
3930 matches!(err, Error::InsufficientDiskSpace(_)),
3931 "expected InsufficientDiskSpace, got: {err}"
3932 );
3933 }
3934
3935 mod external_merkle_fold {
3938 use super::*;
3939 use crate::data::client::merkle::test_support::{
3940 make_prepared_merkle_batch, winner_hash_for,
3941 };
3942
3943 #[test]
3944 fn hash_count_mismatch_is_rejected() {
3945 let batches = vec![make_prepared_merkle_batch(2), make_prepared_merkle_batch(3)];
3946 let err = fold_external_merkle_payments(batches, vec![None]).unwrap_err();
3947 assert!(
3948 err.to_string().contains("winner pool hash entries"),
3949 "unexpected error: {err}"
3950 );
3951 }
3952
3953 #[test]
3954 fn all_unpaid_is_rejected() {
3955 let batches = vec![make_prepared_merkle_batch(2)];
3956 let err = fold_external_merkle_payments(batches, vec![None]).unwrap_err();
3957 assert!(
3958 err.to_string().contains("No merkle sub-batch was paid"),
3959 "unexpected error: {err}"
3960 );
3961 }
3962
3963 #[test]
3967 fn paid_batches_fold_and_unpaid_contribute_no_proofs() {
3968 let paid = make_prepared_merkle_batch(2);
3969 let unpaid = make_prepared_merkle_batch(3);
3970 let winner = winner_hash_for(&paid);
3971 let merged =
3972 fold_external_merkle_payments(vec![paid, unpaid], vec![Some(winner), None])
3973 .unwrap();
3974 assert_eq!(merged.proofs.len(), 2, "proofs cover only the paid batch");
3975 assert_eq!(merged.chunk_count, 2);
3976 }
3977 }
3978
3979 #[test]
3980 fn adaptive_stream_decrypt_batch_size_tracks_fetch_headroom() {
3981 let batch_size = adaptive_stream_decrypt_batch_size(1_000, 64, 10, Some(u64::MAX));
3982
3983 assert_eq!(batch_size, 64 * DOWNLOAD_STREAM_BATCH_FETCH_MULTIPLIER);
3984 }
3985
3986 #[test]
3987 fn adaptive_stream_decrypt_batch_size_caps_to_total_chunks() {
3988 let batch_size = adaptive_stream_decrypt_batch_size(12, 64, 10, Some(u64::MAX));
3989
3990 assert_eq!(batch_size, 12);
3991 }
3992
3993 #[test]
3994 fn adaptive_stream_decrypt_batch_size_honours_configured_floor() {
3995 let batch_size = adaptive_stream_decrypt_batch_size(1_000, 1, 32, None);
3996
3997 assert_eq!(batch_size, 32);
3998 }
3999
4000 #[test]
4001 fn adaptive_stream_decrypt_batch_size_does_not_expand_without_memory_reading() {
4002 let batch_size = adaptive_stream_decrypt_batch_size(1_000, 64, 10, None);
4003
4004 assert_eq!(batch_size, 10);
4005 }
4006
4007 #[test]
4008 fn adaptive_stream_decrypt_batch_size_caps_to_memory_budget() {
4009 let estimated_bytes_per_chunk = (self_encryption::MAX_CHUNK_SIZE as u64)
4010 .saturating_mul(DOWNLOAD_STREAM_BATCH_BYTES_PER_CHUNK_MULTIPLIER)
4011 .max(1);
4012 let usable_memory = estimated_bytes_per_chunk
4013 .saturating_mul(16)
4014 .saturating_mul(DOWNLOAD_STREAM_BATCH_MEMORY_BUDGET_DIVISOR);
4015 let batch_size = adaptive_stream_decrypt_batch_size(1_000, 256, 10, Some(usable_memory));
4016
4017 assert_eq!(batch_size, 16);
4018 }
4019
4020 #[test]
4021 fn adaptive_stream_decrypt_batch_size_keeps_one_chunk_when_memory_is_tight() {
4022 let batch_size = adaptive_stream_decrypt_batch_size(1_000, 64, 10, Some(1));
4023
4024 assert_eq!(batch_size, 1);
4025 }
4026
4027 #[test]
4028 fn cached_merkle_covers_only_when_all_addresses_have_proofs() {
4029 let covered = compute_address(&Bytes::from_static(b"covered"));
4030 let extra = compute_address(&Bytes::from_static(b"extra"));
4031 let missing = compute_address(&Bytes::from_static(b"missing"));
4032 let cached = MerkleBatchPaymentResult {
4033 proofs: HashMap::from([(covered, vec![1]), (extra, vec![2])]),
4034 chunk_count: 2,
4035 storage_cost_atto: "0".to_string(),
4036 gas_cost_wei: 0,
4037 merkle_payment_timestamp: 0,
4038 };
4039
4040 assert!(cached_merkle_covers_addresses(&cached, &[covered]));
4041 assert!(cached_merkle_covers_addresses(&cached, &[covered, extra]));
4042 assert!(!cached_merkle_covers_addresses(
4043 &cached,
4044 &[covered, missing]
4045 ));
4046 }
4047
4048 #[test]
4053 fn partition_addresses_by_proof_splits_paid_and_unpaid() {
4054 let paid_a = [1u8; 32];
4055 let unpaid_b = [2u8; 32];
4056 let paid_c = [3u8; 32];
4057 let unpaid_d = [4u8; 32];
4058 let proofs: HashMap<[u8; 32], Vec<u8>> =
4059 HashMap::from([(paid_a, vec![0xaa]), (paid_c, vec![0xcc])]);
4060
4061 let (to_store, missing) =
4062 partition_addresses_by_proof(&[paid_a, unpaid_b, paid_c, unpaid_d], &proofs);
4063
4064 assert_eq!(to_store, vec![paid_a, paid_c]);
4065 assert_eq!(missing, vec![unpaid_b, unpaid_d]);
4066 }
4067
4068 fn real_refusal() -> String {
4072 ant_protocol::client_update_required_message(1, 2)
4073 }
4074
4075 #[test]
4082 fn the_partial_reason_carries_a_refusal_instead_of_a_bogus_shortfall() {
4083 let refusal = real_refusal();
4084 assert!(
4085 refusal.contains("ant update"),
4086 "the storer wording must carry the instruction: {refusal}"
4087 );
4088
4089 let all_proofless = merkle_partial_reason(3, 3, 4, Some(&refusal));
4091 assert!(all_proofless.contains("ant update"), "{all_proofless}");
4092 assert!(
4093 !all_proofless.contains("short of quorum"),
4094 "{all_proofless}"
4095 );
4096
4097 let mixed = merkle_partial_reason(3, 2, 4, Some(&refusal));
4100 assert!(mixed.contains("ant update"), "{mixed}");
4101 assert!(mixed.contains("2 chunk(s) have no merkle proof"), "{mixed}");
4102 assert!(
4103 mixed.contains("1 chunk(s) short of quorum after 4 attempts"),
4104 "{mixed}"
4105 );
4106
4107 let silent = merkle_partial_reason(2, 2, 4, None);
4109 assert!(!silent.contains("short of quorum"), "{silent}");
4110 assert!(
4111 silent.contains("2 chunk(s) have no merkle proof"),
4112 "{silent}"
4113 );
4114 assert!(!silent.contains("refused"), "{silent}");
4115 }
4116
4117 #[test]
4122 fn the_refusal_is_scoped_before_its_nothing_was_charged_clause_is_quoted() {
4123 let refusal = real_refusal();
4124 assert!(
4125 refusal.contains("nothing was charged"),
4126 "precondition: {refusal}"
4127 );
4128
4129 let reason = merkle_partial_reason(2, 2, 4, Some(&refusal));
4130
4131 let scope = reason
4134 .find("settled for earlier sub-batches")
4135 .expect("the reason must scope the refusal");
4136 let charged = reason
4137 .find("nothing was charged")
4138 .expect("the storer wording must still be quoted in full");
4139 assert!(scope < charged, "scope must precede the claim: {reason}");
4140 }
4141
4142 #[test]
4145 fn the_partial_reason_is_unchanged_when_every_chunk_had_a_proof() {
4146 assert_eq!(
4147 merkle_partial_reason(2, 0, 4, None),
4148 "2 chunk(s) short of quorum after 4 attempts"
4149 );
4150 assert_eq!(
4153 merkle_partial_reason(2, 0, 4, Some(&real_refusal())),
4154 "2 chunk(s) short of quorum after 4 attempts"
4155 );
4156 }
4157
4158 #[test]
4163 fn the_reason_never_claims_a_proofless_chunk_went_unpaid() {
4164 let ours = proofless_clause(2, None).expect("two proofless chunks produce a clause");
4167 assert_eq!(ours, "2 chunk(s) have no merkle proof");
4168 assert!(!ours.contains("paid"), "{ours}");
4169 assert!(!ours.contains("charged"), "{ours}");
4170
4171 assert!(proofless_clause(0, None).is_none());
4172 assert!(
4173 proofless_clause(0, Some(&real_refusal())).is_none(),
4174 "no proofless chunks means no clause, refusal or not"
4175 );
4176 }
4177
4178 #[test]
4183 fn a_fatal_store_abort_still_reports_the_refusal() {
4184 let abort = "merkle chunk store aborted: connection reset";
4185 let refusal = real_refusal();
4186
4187 let both = merkle_fatal_reason(abort, 5, Some(&refusal));
4188 assert!(both.starts_with(abort), "the abort leads: {both}");
4189 assert!(both.contains("ant update"), "{both}");
4190 assert!(both.contains("5 chunk(s) have no merkle proof"), "{both}");
4191
4192 assert_eq!(merkle_fatal_reason(abort, 0, Some(&refusal)), abort);
4194 assert_eq!(merkle_fatal_reason(abort, 0, None), abort);
4195 }
4196
4197 #[test]
4200 fn fold_single_wave_keeps_ok_wave() {
4201 let stored = vec![[1u8; 32], [2u8; 32]];
4202 let stats = WaveAggregateStats {
4203 chunk_attempts_total: 7,
4204 ..Default::default()
4205 };
4206
4207 let outcome = fold_single_wave(Ok((stored.clone(), "100".to_string(), 9, stats))).unwrap();
4208
4209 assert_eq!(outcome.stored, stored);
4210 assert!(outcome.failed.is_empty());
4211 assert_eq!(outcome.storage_atto.to_string(), "100");
4212 assert_eq!(outcome.gas_wei, 9);
4213 assert_eq!(outcome.stats.chunk_attempts_total, 7);
4214 }
4215
4216 #[test]
4221 fn fold_single_wave_folds_partial_upload() {
4222 let stored = vec![[3u8; 32]];
4223 let failed = vec![([4u8; 32], "short of quorum".to_string())];
4224 let err = Error::PartialUpload {
4225 stored: stored.clone(),
4226 stored_count: 1,
4227 failed: failed.clone(),
4228 failed_count: 1,
4229 total_chunks: 2,
4230 spend: Box::new(PartialUploadSpend {
4231 storage_cost_atto: "250".to_string(),
4232 gas_cost_wei: 11,
4233 }),
4234 reason: "wave store failed after retries".to_string(),
4235 };
4236
4237 let outcome = fold_single_wave(Err(err)).unwrap();
4238
4239 assert_eq!(outcome.stored, stored);
4240 assert_eq!(outcome.failed, failed);
4241 assert_eq!(outcome.storage_atto.to_string(), "250");
4242 assert_eq!(outcome.gas_wei, 11);
4243 assert_eq!(outcome.stats.chunk_attempts_total, 0);
4245 }
4246
4247 #[test]
4250 fn fold_single_wave_propagates_fatal_error() {
4251 let result = fold_single_wave(Err(Error::Payment("wallet unavailable".to_string())));
4252
4253 assert!(
4254 matches!(result, Err(Error::Payment(_))),
4255 "fatal payment error must propagate, got: {result:?}"
4256 );
4257 }
4258
4259 #[test]
4265 fn settlement_refusal_after_paid_waves_carries_spend_and_upgrade_instruction() {
4266 let refusal = "your client is too old to pay the current storage rate. Run `ant update`";
4267 let stored = vec![[1u8; 32], [2u8; 32]];
4268 let remaining = [[3u8; 32], [4u8; 32], [5u8; 32]];
4269
4270 let err = settlement_refusal_after_paid_waves(
4271 refusal,
4272 2,
4273 3,
4274 stored.clone(),
4275 stored.len(),
4276 &remaining,
4277 5,
4278 Amount::from(700u64),
4279 13,
4280 );
4281
4282 let Error::PartialUpload {
4283 stored: got_stored,
4284 stored_count,
4285 failed,
4286 failed_count,
4287 total_chunks,
4288 spend,
4289 reason,
4290 } = err
4291 else {
4292 panic!("expected PartialUpload, got: {err:?}");
4293 };
4294 assert_eq!(got_stored, stored);
4295 assert_eq!(stored_count, 2);
4296 assert_eq!(failed_count, 3);
4297 assert_eq!(total_chunks, 5);
4298 let failed_addrs: Vec<[u8; 32]> = failed.iter().map(|(a, _)| *a).collect();
4300 assert_eq!(failed_addrs, remaining.to_vec());
4301 assert!(failed.iter().all(|(_, why)| why.contains("not quoted")));
4302 assert_eq!(spend.storage_cost_atto, "700");
4304 assert_eq!(spend.gas_cost_wei, 13);
4305 assert!(reason.contains("wave 2/3"), "reason: {reason}");
4307 assert!(
4308 reason.contains("2 chunk(s) in earlier wave(s) were already paid"),
4309 "reason: {reason}"
4310 );
4311 assert!(
4312 reason.contains("3 chunk(s) were neither quoted nor paid"),
4313 "reason: {reason}"
4314 );
4315 assert!(reason.contains(refusal), "reason: {reason}");
4316 }
4317
4318 #[test]
4319 fn partition_addresses_by_proof_handles_all_or_nothing() {
4320 let a = [5u8; 32];
4321 let b = [6u8; 32];
4322
4323 let empty: HashMap<[u8; 32], Vec<u8>> = HashMap::new();
4325 let (to_store, missing) = partition_addresses_by_proof(&[a, b], &empty);
4326 assert!(to_store.is_empty());
4327 assert_eq!(missing, vec![a, b]);
4328
4329 let full: HashMap<[u8; 32], Vec<u8>> = HashMap::from([(a, vec![1]), (b, vec![2])]);
4331 let (to_store, missing) = partition_addresses_by_proof(&[a, b], &full);
4332 assert_eq!(to_store, vec![a, b]);
4333 assert!(missing.is_empty());
4334 }
4335
4336 #[test]
4337 fn chunk_spill_round_trip() {
4338 let mut spill = ChunkSpill::new().unwrap();
4339 let data1 = vec![0xAA; 1024];
4340 let data2 = vec![0xBB; 2048];
4341
4342 spill.push(&data1).unwrap();
4343 spill.push(&data2).unwrap();
4344
4345 assert_eq!(spill.len(), 2);
4346 assert_eq!(spill.total_bytes(), 1024 + 2048);
4347 let chunk_entries = spill.chunk_entries().unwrap();
4348 let entry_total: u64 = chunk_entries.iter().map(|(_, size)| *size).sum();
4349 assert_eq!(entry_total, 1024 + 2048);
4350
4351 let chunk1 = spill.read_chunk(spill.addresses.first().unwrap()).unwrap();
4353 assert_eq!(&chunk1[..], &data1[..]);
4354
4355 let chunk2 = spill.read_chunk(spill.addresses.get(1).unwrap()).unwrap();
4356 assert_eq!(&chunk2[..], &data2[..]);
4357
4358 let waves: Vec<_> = spill.addresses.chunks(1).collect();
4360 assert_eq!(waves.len(), 2);
4361 }
4362
4363 #[test]
4364 fn chunk_spill_cleanup_on_drop() {
4365 let dir;
4366 {
4367 let spill = ChunkSpill::new().unwrap();
4368 dir = spill.dir.clone();
4369 assert!(dir.exists());
4370 }
4371 assert!(!dir.exists(), "spill dir should be removed on drop");
4373 }
4374
4375 #[test]
4376 fn chunk_spill_deduplicates_identical_content() {
4377 let mut spill = ChunkSpill::new().unwrap();
4378 let data = vec![0xCC; 512];
4379
4380 spill.push(&data).unwrap();
4381 spill.push(&data).unwrap(); spill.push(&data).unwrap(); assert_eq!(spill.len(), 1, "duplicate chunks should be deduplicated");
4385 assert_eq!(
4386 spill.total_bytes(),
4387 512,
4388 "total_bytes should count unique only"
4389 );
4390
4391 let data2 = vec![0xDD; 256];
4393 spill.push(&data2).unwrap();
4394 assert_eq!(spill.len(), 2);
4395 assert_eq!(spill.total_bytes(), 512 + 256);
4396 }
4397}
4398
4399#[cfg(test)]
4401mod send_assertions {
4402 use super::*;
4403
4404 fn _assert_send<T: Send>(_: &T) {}
4405
4406 #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
4407 async fn _file_upload_is_send(client: &Client) {
4408 let fut = client.file_upload(Path::new("/dev/null"));
4409 _assert_send(&fut);
4410 }
4411
4412 #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
4413 async fn _file_upload_with_mode_is_send(client: &Client) {
4414 let fut = client.file_upload_with_mode(Path::new("/dev/null"), PaymentMode::Auto);
4415 _assert_send(&fut);
4416 }
4417
4418 #[allow(
4419 dead_code,
4420 unreachable_code,
4421 unused_variables,
4422 clippy::diverging_sub_expression
4423 )]
4424 async fn _file_download_is_send(client: &Client) {
4425 let dm: DataMap = todo!();
4426 let fut = client.file_download(&dm, Path::new("/dev/null"));
4427 _assert_send(&fut);
4428 }
4429}
4430
4431impl Client {
4432 async fn upload_merkle_from_spill(
4433 &self,
4434 spill: &ChunkSpill,
4435 addresses: &[[u8; 32]],
4436 batch_result: &MerkleBatchPaymentResult,
4437 already_stored_addresses: &[[u8; 32]],
4438 progress: Option<&mpsc::Sender<UploadEvent>>,
4439 payment_refusal: Option<&str>,
4440 ) -> Result<(usize, String, u128, WaveAggregateStats)> {
4441 let mut total_stored = already_stored_addresses.len();
4442 let total_chunks = total_stored + addresses.len();
4443 let mut stored_addresses: Vec<[u8; 32]> = already_stored_addresses.to_vec();
4444 let mut failed: Vec<([u8; 32], String)> = Vec::new();
4445 let mut agg_stats = WaveAggregateStats::default();
4446
4447 let (to_store, missing_proof) =
4455 partition_addresses_by_proof(addresses, &batch_result.proofs);
4456 if !missing_proof.is_empty() {
4457 match payment_refusal {
4458 Some(reason) => warn!(
4459 "{} chunk(s) lack a merkle proof ({reason}); reporting them as failed",
4460 missing_proof.len()
4461 ),
4462 None => warn!(
4463 "{} chunk(s) lack a merkle proof (partial payment); reporting them as failed",
4464 missing_proof.len()
4465 ),
4466 }
4467 for addr in &missing_proof {
4468 let hex_addr = hex::encode(addr);
4469 failed.push((
4470 *addr,
4471 match payment_refusal {
4472 Some(reason) => format!("No merkle proof for chunk {hex_addr}: {reason}"),
4473 None => format!("Missing merkle proof for chunk {hex_addr}"),
4474 },
4475 ));
4476 }
4477 }
4478
4479 let store_limiter = self.controller().store.clone();
4480
4481 let store_one = |addr: [u8; 32]| {
4495 let limiter = store_limiter.clone();
4496 let proof_bytes = batch_result.proofs.get(&addr).cloned();
4497 async move {
4498 let started = std::time::Instant::now();
4499 let proof = proof_bytes.ok_or_else(|| {
4500 Error::Payment(format!(
4501 "Missing merkle proof for chunk {}",
4502 hex::encode(addr)
4503 ))
4504 })?;
4505 let content = spill.read_chunk(&addr)?;
4506 let peers = self.put_target_peers(&addr).await?;
4507 observe_op(
4508 &limiter,
4509 || async move { self.chunk_put_to_close_group(content, proof, &peers).await },
4510 classify_error,
4511 )
4512 .await
4513 .map(|_| started)
4514 }
4515 };
4516
4517 info!(
4518 "Storing {} chunks (merkle) as a single cap-bounded pass — {total_stored}/{total_chunks} stored so far",
4519 to_store.len()
4520 );
4521
4522 let cap = || merkle_store_cap(store_limiter.current());
4533 let outcome = merkle_store_with_retry(
4534 to_store.clone(),
4535 cap,
4536 1,
4537 std::time::Duration::ZERO,
4538 progress,
4539 total_stored,
4540 total_chunks,
4541 &store_one,
4542 )
4543 .await?;
4544
4545 stored_addresses.extend(&outcome.stored_addresses);
4550 total_stored = outcome.stored;
4551
4552 agg_stats.chunk_attempts_total = agg_stats
4554 .chunk_attempts_total
4555 .saturating_add(outcome.stats.chunk_attempts_total);
4556 agg_stats
4557 .store_durations_ms
4558 .extend(outcome.stats.store_durations_ms);
4559 for (slot, count) in agg_stats
4560 .retries_histogram
4561 .iter_mut()
4562 .zip(outcome.stats.retries_histogram.iter())
4563 {
4564 *slot = slot.saturating_add(*count);
4565 }
4566
4567 if let Some(e) = outcome.fatal {
4568 warn!("merkle store aborted: {e}");
4573 let mut known_failed = failed;
4574 known_failed.extend(outcome.failed_addresses);
4575 return Err(partial_upload_after_fatal(
4576 addresses,
4577 stored_addresses,
4578 total_stored,
4579 total_chunks,
4580 known_failed,
4581 PartialUploadSpend {
4582 storage_cost_atto: batch_result.storage_cost_atto.clone(),
4583 gas_cost_wei: batch_result.gas_cost_wei,
4584 },
4585 merkle_fatal_reason(
4586 &format!("merkle chunk store aborted: {e}"),
4587 missing_proof.len(),
4588 payment_refusal,
4589 ),
4590 ));
4591 }
4592
4593 let deferred: Vec<([u8; 32], String)> = outcome.failed_addresses;
4597
4598 if !deferred.is_empty() {
4604 info!(
4605 "Deferring {} merkle chunk(s) short of quorum for concurrent retry after the store pass",
4606 deferred.len()
4607 );
4608 let dr = merkle_deferred_retry(
4609 deferred,
4610 &DEFERRED_ROUND_DELAYS_SECS,
4611 |n: usize| merkle_store_cap(store_limiter.current()).min(n.max(1)),
4612 progress,
4613 total_stored,
4614 total_chunks,
4615 &store_one,
4616 )
4617 .await?;
4618
4619 stored_addresses.extend(dr.stored_addresses);
4620 total_stored = dr.stored;
4621
4622 agg_stats.chunk_attempts_total = agg_stats
4625 .chunk_attempts_total
4626 .saturating_add(dr.stats.chunk_attempts_total);
4627 agg_stats
4628 .store_durations_ms
4629 .extend(dr.stats.store_durations_ms);
4630 for (slot, count) in agg_stats
4631 .retries_histogram
4632 .iter_mut()
4633 .zip(dr.stats.retries_histogram.iter())
4634 {
4635 *slot = slot.saturating_add(*count);
4636 }
4637
4638 if let Some(reason) = dr.fatal {
4639 warn!("merkle deferred retry aborted: {reason}");
4643 let mut known_failed = failed;
4644 known_failed.extend(dr.failed_addresses);
4645 return Err(partial_upload_after_fatal(
4646 addresses,
4647 stored_addresses,
4648 total_stored,
4649 total_chunks,
4650 known_failed,
4651 PartialUploadSpend {
4652 storage_cost_atto: batch_result.storage_cost_atto.clone(),
4653 gas_cost_wei: batch_result.gas_cost_wei,
4654 },
4655 merkle_fatal_reason(
4656 &format!("merkle chunk store aborted: {reason}"),
4657 missing_proof.len(),
4658 payment_refusal,
4659 ),
4660 ));
4661 }
4662 failed.extend(dr.failed_addresses);
4663 }
4664
4665 if !failed.is_empty() {
4669 let failed_count = failed.len();
4670 let total_attempts = 1 + DEFERRED_ROUND_DELAYS_SECS.len();
4671 let reason = merkle_partial_reason(
4672 failed_count,
4673 missing_proof.len(),
4674 total_attempts,
4675 payment_refusal,
4676 );
4677 warn!(
4678 "merkle upload incomplete: {failed_count}/{total_chunks} chunks failed — {reason}"
4679 );
4680 return Err(Error::PartialUpload {
4681 stored: stored_addresses,
4682 stored_count: total_stored,
4683 failed,
4684 failed_count,
4685 total_chunks,
4686 spend: Box::new(PartialUploadSpend {
4687 storage_cost_atto: batch_result.storage_cost_atto.clone(),
4688 gas_cost_wei: batch_result.gas_cost_wei,
4689 }),
4690 reason,
4691 });
4692 }
4693
4694 Ok((
4695 total_stored,
4696 batch_result.storage_cost_atto.clone(),
4697 batch_result.gas_cost_wei,
4698 agg_stats,
4699 ))
4700 }
4701}