Skip to main content

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    /// The network refused to quote this client because it settles payments
103    /// under superseded rules.
104    ///
105    /// Deliberately terminal. A client that reaches this would pay an amount
106    /// every storer rejects, and merkle payments are not refundable, so
107    /// retrying or falling back to an older request shape would convert a
108    /// clean refusal into destroyed money. The message is the storer's own
109    /// wording, which already tells the user how to upgrade and that nothing
110    /// has been charged.
111    #[error("{0}")]
112    ClientUpdateRequired(String),
113
114    /// A storer declined to quote because *it* settles under older rules than
115    /// this client.
116    ///
117    /// The opposite of [`Self::ClientUpdateRequired`] and deliberately not
118    /// terminal. Nothing is wrong with this client, so the upload should use a
119    /// different peer and say nothing to the user. During a client-first
120    /// rollout most of the fleet is briefly in this state. If too few peers
121    /// remain the operation fails for lack of quotes, which is the correct
122    /// outcome: it fails before any payment rather than after.
123    #[error("{0}")]
124    StorerUpdateRequired(String),
125
126    /// BLS signature verification failed.
127    #[error("signature verification failed: {0}")]
128    SignatureVerification(String),
129
130    /// Self-encryption operation failed.
131    #[error("encryption error: {0}")]
132    Encryption(String),
133
134    /// The operation was cancelled by the caller rather than failing.
135    ///
136    /// Returned, for example, by streaming downloads when the consumer drops
137    /// its receiver (a client disconnect) — distinct from a transport
138    /// [`Error::Network`] failure, since nothing went wrong on the wire.
139    #[error("operation cancelled: {0}")]
140    Cancelled(String),
141
142    /// Data already exists on the network — no payment needed.
143    #[error("already stored on network")]
144    AlreadyStored,
145
146    /// A peer's quote `pub_key` does not BLAKE3-hash to the peer ID. The
147    /// storer would reject any `ProofOfPayment` containing this quote, so
148    /// the client drops the response before payment.
149    #[error("bad quote binding from peer {peer_id}: {detail}")]
150    BadQuoteBinding {
151        /// The peer ID we got the quote from (claimed identity).
152        peer_id: String,
153        /// Diagnostic detail (e.g. "BLAKE3(pub_key) = …, peer_id = …").
154        detail: String,
155    },
156
157    /// ADR-0004: a quote's commitment binding does not hold — its price is not
158    /// `calculate_price(committed_key_count)`, its `(count, pin)` shape is
159    /// incoherent, or a shipped commitment does not match the pinned count/hash.
160    /// The storer's arithmetic gate would reject such a quote, so the client
161    /// drops it before paying ("the client pays nothing it cannot resolve").
162    #[error("bad quote commitment from peer {peer_id}: {detail}")]
163    BadQuoteCommitment {
164        /// The peer ID we got the quote from.
165        peer_id: String,
166        /// Diagnostic detail (which binding rule failed).
167        detail: String,
168    },
169
170    /// Not enough disk space for the operation.
171    #[error("insufficient disk space: {0}")]
172    InsufficientDiskSpace(String),
173
174    /// An external-signer merkle preparation was handed more addresses than a
175    /// single merkle tree can hold.
176    ///
177    /// The wallet path splits an oversized upload into several trees and pays
178    /// each in its own transaction. The external-signer contract is one
179    /// prepared batch → one signature → one payment, so that split has no
180    /// representation there. Raised before any candidate collection or
181    /// on-chain spend, rather than silently paying under a different model.
182    #[error(
183        "merkle batch of {addresses} addresses exceeds the {max_leaves}-leaf limit of a single \
184         merkle tree; external signing cannot span multiple payment transactions"
185    )]
186    MerkleBatchTooLarge {
187        /// Number of addresses the caller asked to prepare.
188        addresses: usize,
189        /// Maximum leaves one merkle tree can hold (`MAX_LEAVES`).
190        max_leaves: usize,
191    },
192
193    /// Cost estimation could not reach a representative quote.
194    ///
195    /// Returned by [`crate::data::Client::estimate_upload_cost`] when every
196    /// sampled chunk address reported `AlreadyStored`, so the network price
197    /// for the remainder of the file cannot be inferred from a sample.
198    /// The attached message describes how many addresses were tried.
199    #[error("cost estimation inconclusive: {0}")]
200    CostEstimationInconclusive(String),
201
202    /// Upload partially succeeded -- some chunks stored, some failed after retries.
203    ///
204    /// The `stored` addresses can be used for progress tracking and resume.
205    #[error(
206        "partial upload: {stored_count}/{total_chunks} stored, {failed_count} failed: {reason}"
207    )]
208    PartialUpload {
209        /// Addresses of successfully stored chunks.
210        stored: Vec<[u8; 32]>,
211        /// Number of successfully stored chunks.
212        stored_count: usize,
213        /// Addresses and error messages of chunks that failed after retries.
214        failed: Vec<([u8; 32], String)>,
215        /// Number of failed chunks.
216        failed_count: usize,
217        /// Total number of chunks the upload was attempting to store.
218        total_chunks: usize,
219        /// On-chain spend incurred so far. Boxed to keep the `Error` enum small
220        /// (the variant is returned in `Result` across the crate; without the
221        /// box the two cost fields would trip `clippy::result_large_err`).
222        spend: Box<PartialUploadSpend>,
223        /// Root cause description.
224        reason: String,
225    },
226}
227
228/// On-chain spend recorded on a [`Error::PartialUpload`].
229///
230/// A partial upload still spends money for the chunks it paid for. In the
231/// single-node path payment precedes store, so this includes a failed wave's
232/// chunks; surfacing it lets the caller report real spend rather than silently
233/// dropping it.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct PartialUploadSpend {
236    /// Storage cost paid on-chain so far, in atto-tokens.
237    pub storage_cost_atto: String,
238    /// Gas cost paid on-chain so far, in wei.
239    pub gas_cost_wei: u128,
240}
241
242// ant-node is only linked when the `devnet` feature is on, so the
243// blanket `From` impl follows that gate. LocalDevnet maps node errors
244// to `Error::Network` via this conversion; default builds never see it.
245#[cfg(feature = "devnet")]
246impl From<ant_node::Error> for Error {
247    fn from(e: ant_node::Error) -> Self {
248        Self::Network(e.to_string())
249    }
250}
251
252#[cfg(test)]
253#[allow(clippy::unwrap_used, clippy::expect_used)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_display_network() {
259        let err = Error::Network("connection refused".to_string());
260        assert_eq!(err.to_string(), "network error: connection refused");
261    }
262
263    #[test]
264    fn test_display_storage() {
265        let err = Error::Storage("disk full".to_string());
266        assert_eq!(err.to_string(), "storage error: disk full");
267    }
268
269    #[test]
270    fn test_display_payment() {
271        let err = Error::Payment("insufficient funds".to_string());
272        assert_eq!(err.to_string(), "payment error: insufficient funds");
273    }
274
275    #[test]
276    fn test_display_protocol() {
277        let err = Error::Protocol("invalid message".to_string());
278        assert_eq!(err.to_string(), "protocol error: invalid message");
279    }
280
281    #[test]
282    fn test_display_invalid_data() {
283        let err = Error::InvalidData("bad hash".to_string());
284        assert_eq!(err.to_string(), "invalid data: bad hash");
285    }
286
287    #[test]
288    fn test_display_not_found() {
289        let err = Error::NotFound("DataMap chunk not found at abcd".to_string());
290        assert_eq!(
291            err.to_string(),
292            "not found: DataMap chunk not found at abcd"
293        );
294    }
295
296    #[test]
297    fn test_display_serialization() {
298        let err = Error::Serialization("decode failed".to_string());
299        assert_eq!(err.to_string(), "serialization error: decode failed");
300    }
301
302    #[test]
303    fn test_display_crypto() {
304        let err = Error::Crypto("key mismatch".to_string());
305        assert_eq!(err.to_string(), "crypto error: key mismatch");
306    }
307
308    #[test]
309    fn test_display_io() {
310        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
311        let err = Error::Io(io_err);
312        assert_eq!(err.to_string(), "I/O error: file missing");
313    }
314
315    #[test]
316    fn test_display_config() {
317        let err = Error::Config("bad value".to_string());
318        assert_eq!(err.to_string(), "configuration error: bad value");
319    }
320
321    #[test]
322    fn test_display_timeout() {
323        let err = Error::Timeout("30s elapsed".to_string());
324        assert_eq!(err.to_string(), "timeout: 30s elapsed");
325    }
326
327    #[test]
328    fn test_display_insufficient_peers() {
329        let err = Error::InsufficientPeers("need 5, got 2".to_string());
330        assert_eq!(err.to_string(), "insufficient peers: need 5, got 2");
331    }
332
333    #[test]
334    fn test_display_close_group_shortfall() {
335        let err = Error::CloseGroupShortfall("Stored on 3 peers, need 4".to_string());
336        assert_eq!(
337            err.to_string(),
338            "close-group PUT shortfall (dial churn): Stored on 3 peers, need 4"
339        );
340    }
341
342    #[test]
343    fn test_display_signature_verification() {
344        let err = Error::SignatureVerification("invalid sig".to_string());
345        assert_eq!(
346            err.to_string(),
347            "signature verification failed: invalid sig"
348        );
349    }
350
351    #[test]
352    fn test_display_encryption() {
353        let err = Error::Encryption("decrypt failed".to_string());
354        assert_eq!(err.to_string(), "encryption error: decrypt failed");
355    }
356
357    #[test]
358    fn test_display_cancelled() {
359        let err = Error::Cancelled("download stream receiver dropped".to_string());
360        assert_eq!(
361            err.to_string(),
362            "operation cancelled: download stream receiver dropped"
363        );
364    }
365
366    #[test]
367    fn test_display_insufficient_disk_space() {
368        let err = Error::InsufficientDiskSpace("need 100 MB but only 10 MB available".to_string());
369        assert_eq!(
370            err.to_string(),
371            "insufficient disk space: need 100 MB but only 10 MB available"
372        );
373    }
374
375    #[test]
376    fn test_display_merkle_batch_too_large() {
377        let err = Error::MerkleBatchTooLarge {
378            addresses: 257,
379            max_leaves: 256,
380        };
381        assert_eq!(
382            err.to_string(),
383            "merkle batch of 257 addresses exceeds the 256-leaf limit of a single merkle tree; \
384             external signing cannot span multiple payment transactions"
385        );
386    }
387
388    #[test]
389    fn test_display_cost_estimation_inconclusive() {
390        let err = Error::CostEstimationInconclusive(
391            "sampled 5 addresses, all already stored".to_string(),
392        );
393        assert_eq!(
394            err.to_string(),
395            "cost estimation inconclusive: sampled 5 addresses, all already stored"
396        );
397    }
398
399    #[test]
400    fn test_from_io_error() {
401        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
402        let err: Error = io_err.into();
403        assert!(matches!(err, Error::Io(_)));
404    }
405}