Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ant-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ serial_test = "3"
anyhow = "1"
alloy = { version = "1.6", features = ["node-bindings"] }
tokio-test = "0.4"
rmp-serde = "1"
# Direct access to BootstrapManager used by the cold-start-from-disk test,
# which populates a cache via `add_peer_trusted` (bypasses Sybil rate limits)
# and then verifies reload after save. Version tracks ant-node's transitive
Expand Down
1 change: 1 addition & 0 deletions ant-core/src/data/client/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ impl Client {
prepared_chunks,
payment_intent,
},
data_map_address: None,
})
}

Expand Down
114 changes: 111 additions & 3 deletions ant-core/src/data/client/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,23 @@ fn check_disk_space_for_spill(file_size: u64) -> Result<()> {
Ok(())
}

/// Whether the data map is published to the network for address-based retrieval.
///
/// A private upload stores only the data chunks and returns the `DataMap` to
/// the caller — only someone holding that `DataMap` can reconstruct the file.
/// A public upload additionally stores the serialized `DataMap` as a chunk on
/// the network, yielding a single chunk address that anyone can use to
/// retrieve the `DataMap` (via [`Client::data_map_fetch`]) and then the file.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Visibility {
/// Keep the data map local; only the holder can retrieve the file.
#[default]
Private,
/// Publish the data map as a network chunk so anyone with the returned
/// address can retrieve and decrypt the file.
Public,
}

/// Estimated cost of uploading a file, returned by
/// [`Client::estimate_upload_cost`].
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
Expand All @@ -431,8 +448,14 @@ pub struct UploadCostEstimate {
pub payment_mode: PaymentMode,
}


/// Result of a file upload: the `DataMap` needed to retrieve the file.
///
/// Marked `#[non_exhaustive]` so adding a new field in future is not a
/// breaking change for downstream consumers that construct or pattern-match
/// on this struct.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FileUploadResult {
/// The data map containing chunk metadata for reconstruction.
pub data_map: DataMap,
Expand All @@ -444,6 +467,14 @@ pub struct FileUploadResult {
pub storage_cost_atto: String,
/// Total gas cost in wei. 0 if no on-chain transactions were made.
pub gas_cost_wei: u128,
/// Chunk address of the serialized `DataMap`, set only for
/// [`Visibility::Public`] uploads. **`Some` means this address is
/// retrievable from the network (via [`Client::data_map_fetch`])**, not
/// necessarily that *this* upload paid to store it — if the serialized
/// `DataMap` hashed to a chunk that was already on the network (same
/// file uploaded before; deterministic via self-encryption), the address
/// is still returned but no storage payment was made for it.
pub data_map_address: Option<[u8; 32]>,
}

/// Payment information for external signing — either wave-batch or merkle.
Expand Down Expand Up @@ -476,12 +507,23 @@ pub enum ExternalPaymentInfo {
/// Note: This struct stays in Rust memory — only the public fields of
/// `payment_info` are sent to the frontend. `PreparedChunk` contains
/// non-serializable network types, so the full struct cannot derive `Serialize`.
///
/// Marked `#[non_exhaustive]` so adding a new field in future is not a
/// breaking change for downstream consumers.
#[derive(Debug)]
#[non_exhaustive]
pub struct PreparedUpload {
/// The data map for later retrieval.
pub data_map: DataMap,
/// Payment information — either wave-batch or merkle depending on chunk count.
pub payment_info: ExternalPaymentInfo,
/// Chunk address of the serialized `DataMap` when this upload was
/// prepared with [`Visibility::Public`]. `Some` means the address is
/// retrievable on the network after finalization — either because this
/// upload paid to store the chunk in `payment_info`, or because the
/// chunk was already on the network (deterministic self-encryption).
/// Carried through to [`FileUploadResult::data_map_address`].
pub data_map_address: Option<[u8; 32]>,
}

/// Return type for [`spawn_file_encryption`]: chunk receiver, `DataMap` oneshot, join handle.
Expand Down Expand Up @@ -771,11 +813,27 @@ impl Client {

/// Phase 1 of external-signer upload: encrypt file and prepare chunks.
///
/// Equivalent to [`Client::file_prepare_upload_with_visibility`] with
/// [`Visibility::Private`] — see that method for details.
pub async fn file_prepare_upload(&self, path: &Path) -> Result<PreparedUpload> {
self.file_prepare_upload_with_visibility(path, Visibility::Private)
.await
}

/// Phase 1 of external-signer upload with explicit [`Visibility`] control.
///
/// Requires an EVM network (for contract price queries) but NOT a wallet.
/// Returns a [`PreparedUpload`] containing the data map, prepared chunks,
/// and a [`PaymentIntent`] that the external signer uses to construct
/// and submit the on-chain payment transaction.
///
/// When `visibility` is [`Visibility::Public`], the serialized `DataMap`
/// is bundled into the payment batch as an additional chunk and its
/// address is recorded on the returned [`PreparedUpload`]. After
/// [`Client::finalize_upload`] (or `_merkle`) succeeds, that address is
/// surfaced via [`FileUploadResult::data_map_address`] so the uploader
/// can share a single address from which anyone can retrieve the file.
///
/// **Memory note:** Encryption uses disk spilling for bounded memory, but
/// the returned [`PreparedUpload`] holds all chunk content in memory (each
/// [`PreparedChunk`] contains a `Bytes` with the full chunk data). This is
Expand All @@ -787,9 +845,13 @@ impl Client {
///
/// Returns an error if there is insufficient disk space, the file cannot
/// be read, encryption fails, or quote collection fails.
pub async fn file_prepare_upload(&self, path: &Path) -> Result<PreparedUpload> {
pub async fn file_prepare_upload_with_visibility(
&self,
path: &Path,
visibility: Visibility,
) -> Result<PreparedUpload> {
debug!(
"Preparing file upload for external signing: {}",
"Preparing file upload for external signing (visibility={visibility:?}): {}",
path.display()
);

Expand All @@ -807,12 +869,35 @@ impl Client {
// Read each chunk from disk and collect quotes concurrently.
// Note: all PreparedChunks accumulate in memory because the external-signer
// protocol requires them for finalize_upload. NOT memory-bounded for large files.
let chunk_data: Vec<Bytes> = spill
let mut chunk_data: Vec<Bytes> = spill
.addresses
.iter()
.map(|addr| spill.read_chunk(addr))
.collect::<std::result::Result<Vec<_>, _>>()?;

// For public uploads, bundle the serialized DataMap as an extra chunk
// in the same payment batch. This lets the external signer pay for
// the data chunks and the DataMap chunk in one flow, and lets the
// finalize step return the DataMap's chunk address as the shareable
// retrieval address.
let data_map_address = match visibility {
Visibility::Private => None,
Visibility::Public => {
let serialized = rmp_serde::to_vec(&data_map).map_err(|e| {
Error::Serialization(format!("Failed to serialize DataMap: {e}"))
})?;
let bytes = Bytes::from(serialized);
let address = compute_address(&bytes);
info!(
"Public upload: bundling DataMap chunk ({} bytes) at address {}",
bytes.len(),
hex::encode(address)
);
chunk_data.push(bytes);
Some(address)
}
};

let chunk_count = chunk_data.len();

let payment_info = if should_use_merkle(chunk_count, PaymentMode::Auto) {
Expand Down Expand Up @@ -857,6 +942,22 @@ impl Client {
}
}

// Surface the "DataMap chunk was already on the network" case
// so debugging "why is data_map_address set but no storage cost
// appears for it?" doesn't require reading the source. See the
// `data_map_address` doc comment for why this is still a valid
// `Some(addr)` outcome.
if let Some(addr) = data_map_address {
if !prepared_chunks.iter().any(|c| c.address == addr) {
info!(
"Public upload: DataMap chunk {} was already stored \
on the network — address is retrievable without a \
new payment",
hex::encode(addr)
);
}
}

let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);

info!(
Expand All @@ -875,6 +976,7 @@ impl Client {
Ok(PreparedUpload {
data_map,
payment_info,
data_map_address,
})
}

Expand All @@ -894,6 +996,7 @@ impl Client {
prepared: PreparedUpload,
tx_hash_map: &HashMap<QuoteHash, TxHash>,
) -> Result<FileUploadResult> {
let data_map_address = prepared.data_map_address;
match prepared.payment_info {
ExternalPaymentInfo::WaveBatch {
prepared_chunks,
Expand Down Expand Up @@ -921,6 +1024,7 @@ impl Client {
payment_mode_used: PaymentMode::Single,
storage_cost_atto: "0".into(),
gas_cost_wei: 0,
data_map_address,
})
}
ExternalPaymentInfo::Merkle { .. } => Err(Error::Payment(
Expand All @@ -947,6 +1051,7 @@ impl Client {
prepared: PreparedUpload,
winner_pool_hash: [u8; 32],
) -> Result<FileUploadResult> {
let data_map_address = prepared.data_map_address;
match prepared.payment_info {
ExternalPaymentInfo::Merkle {
prepared_batch,
Expand All @@ -966,6 +1071,7 @@ impl Client {
payment_mode_used: PaymentMode::Merkle,
storage_cost_atto: "0".into(),
gas_cost_wei: 0,
data_map_address,
})
}
ExternalPaymentInfo::WaveBatch { .. } => Err(Error::Payment(
Expand Down Expand Up @@ -1055,6 +1161,7 @@ impl Client {
payment_mode_used: PaymentMode::Single,
storage_cost_atto: sc,
gas_cost_wei: gc,
data_map_address: None,
});
}
Err(e) => return Err(e),
Expand All @@ -1080,6 +1187,7 @@ impl Client {
payment_mode_used: actual_mode,
storage_cost_atto,
gas_cost_wei,
data_map_address: None,
})
}

Expand Down
2 changes: 1 addition & 1 deletion ant-core/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use client::batch::{finalize_batch_payment, PaidChunk, PaymentIntent, Prepar
pub use client::data::DataUploadResult;
pub use client::file::{
DownloadEvent, ExternalPaymentInfo, FileUploadResult, PreparedUpload, UploadCostEstimate,
UploadEvent,
UploadEvent, Visibility,
};
pub use client::merkle::{
finalize_merkle_batch, MerkleBatchPaymentResult, PaymentMode, PreparedMerkleBatch,
Expand Down
Loading