ant_core/data/error.rs
1//! Error types for data operations.
2
3use thiserror::Error;
4
5/// Result type alias using the data Error type.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur in data operations.
9#[derive(Error, Debug)]
10pub enum Error {
11 /// Network operation failed.
12 #[error("network error: {0}")]
13 Network(String),
14
15 /// Storage operation failed.
16 #[error("storage error: {0}")]
17 Storage(String),
18
19 /// Payment operation failed.
20 #[error("payment error: {0}")]
21 Payment(String),
22
23 /// Protocol error.
24 #[error("protocol error: {0}")]
25 Protocol(String),
26
27 /// A remote node rejected a chunk PUT at the application layer.
28 ///
29 /// The node responded with a structured `ProtocolError`, so the
30 /// transport round-trip succeeded — this is an application-level
31 /// rejection (payment-failed, storage/disk-full, quote-stale,
32 /// merkle-pool-rejected), NOT evidence the client is sending too
33 /// fast. It therefore classifies as `Outcome::ApplicationError`
34 /// (see `classify_error`) and does not push the adaptive store
35 /// limiter down. The structured `source` is preserved (rather than
36 /// flattened into `Protocol`) so the controller — and a future
37 /// full-node skip-list (V2-469) — can key on the reason.
38 #[error("remote PUT rejected for {address}: {source}")]
39 RemotePut {
40 /// Hex-encoded chunk address the rejection was for.
41 address: String,
42 /// The structured remote rejection reason.
43 source: ant_protocol::ProtocolError,
44 },
45
46 /// A chunk PUT missed its close-group quorum, and the shortfall was
47 /// caused by close-group **dial/relay churn** — dead or stale relayed
48 /// peer addresses that could not be dialled — with **no** evidence of
49 /// local backpressure (no PUT-response timeouts among the failures).
50 ///
51 /// This is remote peer churn (the same dead relayed DHT addresses as
52 /// V2-551), NOT evidence the client is sending too fast. More local send
53 /// concurrency neither causes nor fixes it, so it classifies as
54 /// `Outcome::ApplicationError` (see `classify_error`) and does NOT push
55 /// the adaptive store limiter down (V2-554). Distinct from
56 /// [`Error::InsufficientPeers`], which a close-group shortfall keeps when
57 /// any PUT-response *timeout* is present (genuine local backpressure that
58 /// must still cut the cap). Like `InsufficientPeers`, it is a recoverable
59 /// quorum shortfall and is deferred/retried, not fatal.
60 #[error("close-group PUT shortfall (dial churn): {0}")]
61 CloseGroupShortfall(String),
62
63 /// Invalid data received.
64 #[error("invalid data: {0}")]
65 InvalidData(String),
66
67 /// The requested record does not exist on the network.
68 ///
69 /// A well-formed address with nothing stored at it — e.g. a `DataMap`
70 /// chunk lookup or a reconstruction fetch that came back empty from
71 /// every queried peer. Distinct from [`Error::InvalidData`], which means
72 /// content WAS retrieved but is malformed or fails integrity checks, so
73 /// callers can show "not found — check the address" instead of a
74 /// caller-bug message.
75 #[error("not found: {0}")]
76 NotFound(String),
77
78 /// Serialization error.
79 #[error("serialization error: {0}")]
80 Serialization(String),
81
82 /// Cryptographic error.
83 #[error("crypto error: {0}")]
84 Crypto(String),
85
86 /// I/O error.
87 #[error("I/O error: {0}")]
88 Io(#[from] std::io::Error),
89
90 /// Configuration error.
91 #[error("configuration error: {0}")]
92 Config(String),
93
94 /// Timeout waiting for a response.
95 #[error("timeout: {0}")]
96 Timeout(String),
97
98 /// Insufficient peers for the operation.
99 #[error("insufficient peers: {0}")]
100 InsufficientPeers(String),
101
102 /// BLS signature verification failed.
103 #[error("signature verification failed: {0}")]
104 SignatureVerification(String),
105
106 /// Self-encryption operation failed.
107 #[error("encryption error: {0}")]
108 Encryption(String),
109
110 /// The operation was cancelled by the caller rather than failing.
111 ///
112 /// Returned, for example, by streaming downloads when the consumer drops
113 /// its receiver (a client disconnect) — distinct from a transport
114 /// [`Error::Network`] failure, since nothing went wrong on the wire.
115 #[error("operation cancelled: {0}")]
116 Cancelled(String),
117
118 /// Data already exists on the network — no payment needed.
119 #[error("already stored on network")]
120 AlreadyStored,
121
122 /// A peer's quote `pub_key` does not BLAKE3-hash to the peer ID. The
123 /// storer would reject any `ProofOfPayment` containing this quote, so
124 /// the client drops the response before payment.
125 #[error("bad quote binding from peer {peer_id}: {detail}")]
126 BadQuoteBinding {
127 /// The peer ID we got the quote from (claimed identity).
128 peer_id: String,
129 /// Diagnostic detail (e.g. "BLAKE3(pub_key) = …, peer_id = …").
130 detail: String,
131 },
132
133 /// ADR-0004: a quote's commitment binding does not hold — its price is not
134 /// `calculate_price(committed_key_count)`, its `(count, pin)` shape is
135 /// incoherent, or a shipped commitment does not match the pinned count/hash.
136 /// The storer's arithmetic gate would reject such a quote, so the client
137 /// drops it before paying ("the client pays nothing it cannot resolve").
138 #[error("bad quote commitment from peer {peer_id}: {detail}")]
139 BadQuoteCommitment {
140 /// The peer ID we got the quote from.
141 peer_id: String,
142 /// Diagnostic detail (which binding rule failed).
143 detail: String,
144 },
145
146 /// Not enough disk space for the operation.
147 #[error("insufficient disk space: {0}")]
148 InsufficientDiskSpace(String),
149
150 /// An external-signer merkle preparation was handed more addresses than a
151 /// single merkle tree can hold.
152 ///
153 /// The wallet path splits an oversized upload into several trees and pays
154 /// each in its own transaction. The external-signer contract is one
155 /// prepared batch → one signature → one payment, so that split has no
156 /// representation there. Raised before any candidate collection or
157 /// on-chain spend, rather than silently paying under a different model.
158 #[error(
159 "merkle batch of {addresses} addresses exceeds the {max_leaves}-leaf limit of a single \
160 merkle tree; external signing cannot span multiple payment transactions"
161 )]
162 MerkleBatchTooLarge {
163 /// Number of addresses the caller asked to prepare.
164 addresses: usize,
165 /// Maximum leaves one merkle tree can hold (`MAX_LEAVES`).
166 max_leaves: usize,
167 },
168
169 /// Cost estimation could not reach a representative quote.
170 ///
171 /// Returned by [`crate::data::Client::estimate_upload_cost`] when every
172 /// sampled chunk address reported `AlreadyStored`, so the network price
173 /// for the remainder of the file cannot be inferred from a sample.
174 /// The attached message describes how many addresses were tried.
175 #[error("cost estimation inconclusive: {0}")]
176 CostEstimationInconclusive(String),
177
178 /// Upload partially succeeded -- some chunks stored, some failed after retries.
179 ///
180 /// The `stored` addresses can be used for progress tracking and resume.
181 #[error(
182 "partial upload: {stored_count}/{total_chunks} stored, {failed_count} failed: {reason}"
183 )]
184 PartialUpload {
185 /// Addresses of successfully stored chunks.
186 stored: Vec<[u8; 32]>,
187 /// Number of successfully stored chunks.
188 stored_count: usize,
189 /// Addresses and error messages of chunks that failed after retries.
190 failed: Vec<([u8; 32], String)>,
191 /// Number of failed chunks.
192 failed_count: usize,
193 /// Total number of chunks the upload was attempting to store.
194 total_chunks: usize,
195 /// On-chain spend incurred so far. Boxed to keep the `Error` enum small
196 /// (the variant is returned in `Result` across the crate; without the
197 /// box the two cost fields would trip `clippy::result_large_err`).
198 spend: Box<PartialUploadSpend>,
199 /// Root cause description.
200 reason: String,
201 },
202}
203
204/// On-chain spend recorded on a [`Error::PartialUpload`].
205///
206/// A partial upload still spends money for the chunks it paid for. In the
207/// single-node path payment precedes store, so this includes a failed wave's
208/// chunks; surfacing it lets the caller report real spend rather than silently
209/// dropping it.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct PartialUploadSpend {
212 /// Storage cost paid on-chain so far, in atto-tokens.
213 pub storage_cost_atto: String,
214 /// Gas cost paid on-chain so far, in wei.
215 pub gas_cost_wei: u128,
216}
217
218// ant-node is only linked when the `devnet` feature is on, so the
219// blanket `From` impl follows that gate. LocalDevnet maps node errors
220// to `Error::Network` via this conversion; default builds never see it.
221#[cfg(feature = "devnet")]
222impl From<ant_node::Error> for Error {
223 fn from(e: ant_node::Error) -> Self {
224 Self::Network(e.to_string())
225 }
226}
227
228#[cfg(test)]
229#[allow(clippy::unwrap_used, clippy::expect_used)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_display_network() {
235 let err = Error::Network("connection refused".to_string());
236 assert_eq!(err.to_string(), "network error: connection refused");
237 }
238
239 #[test]
240 fn test_display_storage() {
241 let err = Error::Storage("disk full".to_string());
242 assert_eq!(err.to_string(), "storage error: disk full");
243 }
244
245 #[test]
246 fn test_display_payment() {
247 let err = Error::Payment("insufficient funds".to_string());
248 assert_eq!(err.to_string(), "payment error: insufficient funds");
249 }
250
251 #[test]
252 fn test_display_protocol() {
253 let err = Error::Protocol("invalid message".to_string());
254 assert_eq!(err.to_string(), "protocol error: invalid message");
255 }
256
257 #[test]
258 fn test_display_invalid_data() {
259 let err = Error::InvalidData("bad hash".to_string());
260 assert_eq!(err.to_string(), "invalid data: bad hash");
261 }
262
263 #[test]
264 fn test_display_not_found() {
265 let err = Error::NotFound("DataMap chunk not found at abcd".to_string());
266 assert_eq!(
267 err.to_string(),
268 "not found: DataMap chunk not found at abcd"
269 );
270 }
271
272 #[test]
273 fn test_display_serialization() {
274 let err = Error::Serialization("decode failed".to_string());
275 assert_eq!(err.to_string(), "serialization error: decode failed");
276 }
277
278 #[test]
279 fn test_display_crypto() {
280 let err = Error::Crypto("key mismatch".to_string());
281 assert_eq!(err.to_string(), "crypto error: key mismatch");
282 }
283
284 #[test]
285 fn test_display_io() {
286 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
287 let err = Error::Io(io_err);
288 assert_eq!(err.to_string(), "I/O error: file missing");
289 }
290
291 #[test]
292 fn test_display_config() {
293 let err = Error::Config("bad value".to_string());
294 assert_eq!(err.to_string(), "configuration error: bad value");
295 }
296
297 #[test]
298 fn test_display_timeout() {
299 let err = Error::Timeout("30s elapsed".to_string());
300 assert_eq!(err.to_string(), "timeout: 30s elapsed");
301 }
302
303 #[test]
304 fn test_display_insufficient_peers() {
305 let err = Error::InsufficientPeers("need 5, got 2".to_string());
306 assert_eq!(err.to_string(), "insufficient peers: need 5, got 2");
307 }
308
309 #[test]
310 fn test_display_close_group_shortfall() {
311 let err = Error::CloseGroupShortfall("Stored on 3 peers, need 4".to_string());
312 assert_eq!(
313 err.to_string(),
314 "close-group PUT shortfall (dial churn): Stored on 3 peers, need 4"
315 );
316 }
317
318 #[test]
319 fn test_display_signature_verification() {
320 let err = Error::SignatureVerification("invalid sig".to_string());
321 assert_eq!(
322 err.to_string(),
323 "signature verification failed: invalid sig"
324 );
325 }
326
327 #[test]
328 fn test_display_encryption() {
329 let err = Error::Encryption("decrypt failed".to_string());
330 assert_eq!(err.to_string(), "encryption error: decrypt failed");
331 }
332
333 #[test]
334 fn test_display_cancelled() {
335 let err = Error::Cancelled("download stream receiver dropped".to_string());
336 assert_eq!(
337 err.to_string(),
338 "operation cancelled: download stream receiver dropped"
339 );
340 }
341
342 #[test]
343 fn test_display_insufficient_disk_space() {
344 let err = Error::InsufficientDiskSpace("need 100 MB but only 10 MB available".to_string());
345 assert_eq!(
346 err.to_string(),
347 "insufficient disk space: need 100 MB but only 10 MB available"
348 );
349 }
350
351 #[test]
352 fn test_display_merkle_batch_too_large() {
353 let err = Error::MerkleBatchTooLarge {
354 addresses: 257,
355 max_leaves: 256,
356 };
357 assert_eq!(
358 err.to_string(),
359 "merkle batch of 257 addresses exceeds the 256-leaf limit of a single merkle tree; \
360 external signing cannot span multiple payment transactions"
361 );
362 }
363
364 #[test]
365 fn test_display_cost_estimation_inconclusive() {
366 let err = Error::CostEstimationInconclusive(
367 "sampled 5 addresses, all already stored".to_string(),
368 );
369 assert_eq!(
370 err.to_string(),
371 "cost estimation inconclusive: sampled 5 addresses, all already stored"
372 );
373 }
374
375 #[test]
376 fn test_from_io_error() {
377 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
378 let err: Error = io_err.into();
379 assert!(matches!(err, Error::Io(_)));
380 }
381}