Skip to main content

ant_core/data/client/
data.rs

1//! In-memory data operations using self-encryption.
2//!
3//! Upload and download raw byte data. Content is encrypted via
4//! convergent encryption and stored as content-addressed chunks.
5//! Use this when you already have data in memory (e.g., `Bytes`).
6//! For file-based streaming uploads that avoid loading the entire
7//! file into memory, see the `file` module.
8
9#[cfg(feature = "native")]
10use crate::data::client::adaptive::observe_op;
11#[cfg(feature = "native")]
12use crate::data::client::batch::{PaymentIntent, PreparedChunk};
13#[cfg(feature = "native")]
14use crate::data::client::classify_error;
15#[cfg(feature = "native")]
16use crate::data::client::file::{ExternalPaymentInfo, PreparedUpload, Visibility};
17use crate::data::client::merkle::PaymentMode;
18use crate::data::client::Client;
19use crate::data::error::{Error, Result};
20use ant_protocol::compute_address;
21use bytes::Bytes;
22#[cfg(feature = "native")]
23use futures::stream::StreamExt;
24use self_encryption::{encrypt, DataMap};
25use std::num::NonZeroUsize;
26use tracing::{debug, info};
27
28/// Result of an in-memory data upload: the `DataMap` needed to retrieve the data.
29#[derive(Debug, Clone)]
30pub struct DataUploadResult {
31    /// The data map containing chunk metadata for reconstruction.
32    pub data_map: DataMap,
33    /// Number of chunks stored on the network.
34    pub chunks_stored: usize,
35    /// Which payment mode was actually used (not just requested).
36    pub payment_mode_used: PaymentMode,
37}
38
39impl Client {
40    /// Upload in-memory data to the network using self-encryption.
41    ///
42    /// The content is encrypted and split into chunks, each stored
43    /// as a content-addressed chunk on the network. Returns a `DataMap`
44    /// that can be used to retrieve and decrypt the data.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if encryption fails or any chunk cannot be stored.
49    pub async fn data_upload(&self, content: Bytes) -> Result<DataUploadResult> {
50        let content_len = content.len();
51        debug!("Encrypting data ({content_len} bytes)");
52
53        let (data_map, encrypted_chunks) = encrypt(content)
54            .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?;
55
56        info!("Data encrypted into {} chunks", encrypted_chunks.len());
57
58        let chunk_contents: Vec<Bytes> = encrypted_chunks
59            .into_iter()
60            .map(|chunk| chunk.content)
61            .collect();
62
63        let (addresses, _storage_cost, _gas_cost) =
64            self.batch_upload_chunks(chunk_contents).await?;
65        let chunks_stored = addresses.len();
66
67        info!("Data uploaded: {chunks_stored} chunks stored ({content_len} bytes original)");
68
69        Ok(DataUploadResult {
70            data_map,
71            chunks_stored,
72            payment_mode_used: PaymentMode::Single,
73        })
74    }
75
76    /// Upload in-memory data with a specific payment mode.
77    ///
78    /// When `mode` is `Auto` and the chunk count >= threshold, or when `mode`
79    /// is `Merkle`, this buffers all chunks and pays via a single merkle
80    /// batch transaction. Otherwise falls back to per-chunk payment.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if encryption fails or any chunk cannot be stored.
85    pub async fn data_upload_with_mode(
86        &self,
87        content: Bytes,
88        mode: PaymentMode,
89    ) -> Result<DataUploadResult> {
90        let (data_map, encrypted) =
91            encrypt(content).map_err(|e| Error::Encryption(e.to_string()))?;
92        let chunks = encrypted
93            .into_iter()
94            .map(|chunk| chunk.content)
95            .collect::<Vec<_>>();
96        let records = chunks
97            .iter()
98            .enumerate()
99            .map(|(index, bytes)| super::upload::UploadRecord {
100                address: compute_address(bytes),
101                size: bytes.len() as u64,
102                index,
103            })
104            .collect();
105        let adapter = super::upload::MemoryUploadAdapter {
106            client: self,
107            chunks: &chunks,
108            progress: None,
109            stored_offset: 0,
110            file_total: chunks.len(),
111            resume_key: None,
112        };
113        let outcome = self
114            .upload_records(records, &mut Default::default(), &adapter, mode)
115            .await?;
116        Ok(DataUploadResult {
117            data_map,
118            chunks_stored: outcome.addresses.len(),
119            payment_mode_used: outcome.mode,
120        })
121    }
122
123    /// Phase 1 of external-signer data upload: encrypt and collect quotes.
124    ///
125    /// Equivalent to [`Client::data_prepare_upload_with_visibility`] with
126    /// [`Visibility::Private`] — see that method for details.
127    #[cfg(feature = "native")]
128    pub async fn data_prepare_upload(&self, content: Bytes) -> Result<PreparedUpload> {
129        self.data_prepare_upload_with_visibility(content, Visibility::Private)
130            .await
131    }
132
133    /// Phase 1 of external-signer data upload with explicit [`Visibility`] control.
134    ///
135    /// Encrypts in-memory data via self-encryption, then collects storage
136    /// quotes for each chunk without making any on-chain payment. Returns
137    /// a [`PreparedUpload`] containing the data map and a [`PaymentIntent`]
138    /// with the payment details for external signing.
139    ///
140    /// When `visibility` is [`Visibility::Public`], the serialized `DataMap`
141    /// is bundled into the payment batch as an additional chunk and its
142    /// address is recorded on the returned [`PreparedUpload`]. After
143    /// [`Client::finalize_upload`] succeeds, that address is surfaced via
144    /// [`crate::data::client::file::FileUploadResult::data_map_address`] so
145    /// the uploader can share a single address from which anyone can retrieve
146    /// the data.
147    ///
148    /// Wave-batch payment only — the in-memory data path does not currently
149    /// support merkle batching. Use [`Client::file_prepare_upload_with_visibility`]
150    /// for merkle-eligible public uploads.
151    ///
152    /// After the caller signs and submits the payment transaction, call
153    /// [`Client::finalize_upload`] with the tx hashes to complete storage.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if encryption fails, DataMap serialization fails
158    /// (public only), or quote collection fails.
159    #[cfg(feature = "native")]
160    pub async fn data_prepare_upload_with_visibility(
161        &self,
162        content: Bytes,
163        visibility: Visibility,
164    ) -> Result<PreparedUpload> {
165        let content_len = content.len();
166        debug!("Preparing data upload for external signing (visibility={visibility:?}, {content_len} bytes)");
167
168        let (data_map, encrypted_chunks) = encrypt(content)
169            .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?;
170
171        let mut chunk_contents: Vec<Bytes> = encrypted_chunks
172            .into_iter()
173            .map(|chunk| chunk.content)
174            .collect();
175
176        info!("Data encrypted into {} chunks", chunk_contents.len());
177
178        // For public uploads, bundle the serialized DataMap as an extra chunk
179        // in the same payment batch. This lets the external signer pay for
180        // the data chunks and the DataMap chunk in one flow, and lets the
181        // finalize step return the DataMap's chunk address as the shareable
182        // retrieval address.
183        let data_map_address = match visibility {
184            Visibility::Private => None,
185            Visibility::Public => {
186                let (address, bytes) = crate::client_engine::files::public_map_record(&data_map)
187                    .map_err(Error::Serialization)?;
188                info!(
189                    "Public upload: bundling DataMap chunk ({} bytes) at address {}",
190                    bytes.len(),
191                    hex::encode(address)
192                );
193                chunk_contents.push(bytes);
194                Some(address)
195            }
196        };
197
198        let chunk_count = chunk_contents.len();
199        let chunks_with_addr: Vec<(Bytes, [u8; 32])> = chunk_contents
200            .into_iter()
201            .map(|content| {
202                let address = compute_address(&content);
203                (content, address)
204            })
205            .collect();
206
207        let quote_limiter = self.controller().quote.clone();
208        let quote_concurrency = quote_limiter.current().min(chunk_count.max(1));
209        let results: Vec<([u8; 32], Result<Option<PreparedChunk>>)> =
210            crate::client_engine::bounded_unordered(
211                chunks_with_addr.into_iter().map(|(content, address)| {
212                    let limiter = quote_limiter.clone();
213                    async move {
214                        let result = observe_op(
215                            &limiter,
216                            || async move { self.prepare_chunk_payment(content).await },
217                            classify_error,
218                        )
219                        .await;
220                        (address, result)
221                    }
222                }),
223                quote_concurrency,
224            )
225            .collect()
226            .await;
227
228        let mut prepared_chunks = Vec::with_capacity(results.len());
229        let mut already_stored_addresses = Vec::new();
230        for (address, result) in results {
231            match result? {
232                Some(prepared) => prepared_chunks.push(prepared),
233                None => already_stored_addresses.push(address),
234            }
235        }
236
237        if let Some(addr) = data_map_address {
238            if already_stored_addresses.contains(&addr) {
239                info!(
240                    "Public upload: DataMap chunk {} was already stored \
241                     on the network — address is retrievable without a \
242                     new payment",
243                    hex::encode(addr)
244                );
245            }
246        }
247
248        let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);
249
250        info!(
251            "Data prepared for external signing: {} chunks, {} already stored, total {} atto ({content_len} bytes)",
252            prepared_chunks.len(),
253            already_stored_addresses.len(),
254            payment_intent.total_amount,
255        );
256
257        Ok(PreparedUpload {
258            data_map,
259            payment_info: ExternalPaymentInfo::WaveBatch {
260                prepared_chunks,
261                payment_intent,
262            },
263            data_map_address,
264            already_stored_addresses,
265            total_chunks: chunk_count,
266        })
267    }
268
269    /// Store a `DataMap` on the network as a public chunk.
270    ///
271    /// The serialized `DataMap` is stored as a regular content-addressed chunk.
272    /// Anyone who knows the returned address can retrieve and use the `DataMap`
273    /// to download the original data.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if serialization or the chunk store fails.
278    pub async fn data_map_store(&self, data_map: &DataMap) -> Result<[u8; 32]> {
279        let (_, serialized) = crate::client_engine::files::public_map_record(data_map)
280            .map_err(Error::Serialization)?;
281
282        info!(
283            "Storing DataMap as public chunk ({} bytes serialized)",
284            serialized.len()
285        );
286
287        self.chunk_put(serialized).await
288    }
289
290    /// Fetch a `DataMap` from the network by its chunk address.
291    ///
292    /// Retrieves the chunk at `address` and deserializes it as a `DataMap`.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`Error::NotFound`] if no chunk exists at `address`; other
297    /// errors if retrieval or deserialization fails.
298    pub async fn data_map_fetch(&self, address: &[u8; 32]) -> Result<DataMap> {
299        let chunk = self.chunk_get(address).await?.ok_or_else(|| {
300            Error::NotFound(format!(
301                "DataMap chunk not found at {}",
302                hex::encode(address)
303            ))
304        })?;
305
306        decode_data_map_chunk(&chunk.content)
307    }
308
309    /// Fetch a `DataMap` from the network by trying the requested number
310    /// of closest peers for the DataMap chunk.
311    ///
312    /// # Errors
313    ///
314    /// Returns [`Error::NotFound`] if no chunk exists at `address`; other
315    /// errors if retrieval or deserialization fails.
316    pub async fn data_map_fetch_from_closest_peers(
317        &self,
318        address: &[u8; 32],
319        peer_count: NonZeroUsize,
320    ) -> Result<DataMap> {
321        let chunk = self
322            .chunk_get_from_closest_peers(address, peer_count.get())
323            .await?
324            .ok_or_else(|| {
325                Error::NotFound(format!(
326                    "DataMap chunk not found at {}",
327                    hex::encode(address)
328                ))
329            })?;
330
331        decode_data_map_chunk(&chunk.content)
332    }
333
334    /// Download and decrypt data from the network using its `DataMap`.
335    ///
336    /// Retrieves all chunks referenced by the data map, then decrypts
337    /// and reassembles the original content. Fetches chunks concurrently;
338    /// the fan-out is sized by the adaptive controller's `fetch` channel
339    /// and ramps up under healthy conditions.
340    ///
341    /// Large uploads produce a *shrunk* (child) `DataMap` whose `infos()`
342    /// reference wrapper chunks rather than the root content chunks. Such a
343    /// map is resolved back to its root form before download, keeping this
344    /// primitive symmetric with `data_upload`.
345    ///
346    /// Map resolution and network fetching are fully async and also work on a
347    /// current-thread runtime. The same workflow drives browser downloads.
348    ///
349    /// # Errors
350    /// Returns the underlying fetch error, or an encryption error for invalid
351    /// datamaps and content that fails verification/decryption.
352    pub async fn data_download(&self, data_map: &DataMap) -> Result<Bytes> {
353        self.data_download_with_concurrency(data_map, usize::MAX)
354            .await
355    }
356
357    /// Download data with an upper bound on concurrent record fetches.
358    pub async fn data_download_with_concurrency(
359        &self,
360        data_map: &DataMap,
361        concurrency: usize,
362    ) -> Result<Bytes> {
363        self.data_download_with_progress(data_map, concurrency, &|_, _| {})
364            .await
365    }
366
367    /// Internal observer for verified records; reconstruction still uses the shared engine.
368    pub(crate) async fn data_download_with_progress(
369        &self,
370        data_map: &DataMap,
371        concurrency: usize,
372        progress: &impl Fn(usize, usize),
373    ) -> Result<Bytes> {
374        if concurrency == 0 {
375            return Err(Error::Config(
376                "download concurrency must be positive".into(),
377            ));
378        }
379        let received = std::sync::Mutex::new(std::collections::HashSet::new());
380        let total = data_map.infos().len();
381        progress(0, total);
382        crate::client_engine::files::download(
383            data_map,
384            &|address| {
385                let received = &received;
386                async move {
387                    let bytes = self.fetch_data_record(address).await?;
388                    let mut received = received.lock().unwrap_or_else(|error| error.into_inner());
389                    if received.insert(address) {
390                        let completed = data_map
391                            .infos()
392                            .iter()
393                            .filter(|info| received.contains(&info.dst_hash.0))
394                            .count();
395                        drop(received);
396                        progress(completed, total);
397                    }
398                    Ok(bytes)
399                }
400            },
401            &|| self.controller().fetch.current().min(concurrency),
402            &crate::runtime::sleep,
403            retry_data_fetch,
404        )
405        .await
406        .map_err(map_read_error)
407    }
408
409    /// Download a plaintext byte range using the shared streaming reader.
410    /// Resolves child maps first and fetches only records overlapping the range.
411    /// Length is clamped at EOF; a start at or beyond EOF returns empty bytes.
412    ///
413    /// # Errors
414    /// Returns fetch, datamap validation or decryption errors.
415    pub async fn data_download_range(
416        &self,
417        data_map: &DataMap,
418        start: usize,
419        length: usize,
420    ) -> Result<Bytes> {
421        let fetch = |address| self.fetch_data_record(address);
422        let cap = || self.controller().fetch.current();
423        let root = crate::client_engine::files::resolve(data_map, &fetch, &cap)
424            .await
425            .map_err(map_read_error)?;
426        crate::client_engine::files::read_range(
427            &root,
428            start,
429            length,
430            &fetch,
431            &cap,
432            &crate::runtime::sleep,
433            retry_data_fetch,
434        )
435        .await
436        .map_err(map_read_error)
437    }
438
439    async fn fetch_data_record(&self, address: [u8; 32]) -> Result<Bytes> {
440        self.chunk_get_observed(&address)
441            .await?
442            .map(|chunk| chunk.content)
443            .ok_or_else(|| {
444                Error::NotFound(format!(
445                    "Missing chunk {} required for data reconstruction",
446                    hex::encode(address)
447                ))
448            })
449    }
450}
451
452fn retry_data_fetch(error: &Error) -> bool {
453    matches!(
454        error,
455        Error::NotFound(_)
456            | Error::Timeout(_)
457            | Error::Network(_)
458            | Error::Protocol(_)
459            | Error::Storage(_)
460            | Error::Io(_)
461            | Error::InsufficientPeers(_)
462    )
463}
464
465pub(super) fn map_read_error(error: crate::client_engine::files::ReadError<Error>) -> Error {
466    match error {
467        crate::client_engine::files::ReadError::Fetch(error) => error,
468        crate::client_engine::files::ReadError::Invalid(error) => Error::Encryption(error),
469    }
470}
471
472fn decode_data_map_chunk(content: &[u8]) -> Result<DataMap> {
473    crate::client_engine::files::decode_map(content).map_err(Error::Serialization)
474}
475
476/// Compile-time assertions that Client method futures are Send.
477///
478/// These methods are called from axum handlers and tokio::spawn contexts
479/// that require Send + 'static. The async closures inside stream
480/// combinators must not capture references with concrete lifetimes
481/// (HRTB issue). If any of these checks fail, the stream closures
482/// need restructuring to use owned values instead of references.
483#[cfg(test)]
484mod send_assertions {
485    use super::*;
486
487    fn _assert_send<T: Send>(_: &T) {}
488
489    #[allow(
490        dead_code,
491        unreachable_code,
492        unused_variables,
493        clippy::diverging_sub_expression
494    )]
495    async fn _data_download_is_send(client: &Client) {
496        let dm: DataMap = todo!();
497        let fut = client.data_download(&dm);
498        _assert_send(&fut);
499    }
500
501    #[allow(
502        dead_code,
503        unreachable_code,
504        unused_variables,
505        clippy::diverging_sub_expression
506    )]
507    async fn _data_download_range_is_send(client: &Client) {
508        let dm: DataMap = todo!();
509        _assert_send(&client.data_download_range(&dm, 0, 1024));
510    }
511
512    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
513    async fn _data_upload_is_send(client: &Client) {
514        let fut = client.data_upload(Bytes::new());
515        _assert_send(&fut);
516    }
517
518    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
519    async fn _data_upload_with_mode_is_send(client: &Client) {
520        let fut = client.data_upload_with_mode(Bytes::new(), PaymentMode::Auto);
521        _assert_send(&fut);
522    }
523
524    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
525    async fn _data_prepare_upload_is_send(client: &Client) {
526        let fut = client.data_prepare_upload(Bytes::new());
527        _assert_send(&fut);
528    }
529
530    #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)]
531    async fn _data_prepare_upload_with_visibility_is_send(client: &Client) {
532        let fut = client.data_prepare_upload_with_visibility(Bytes::new(), Visibility::Public);
533        _assert_send(&fut);
534    }
535}