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    /// Invalid data received.
47    #[error("invalid data: {0}")]
48    InvalidData(String),
49
50    /// Serialization error.
51    #[error("serialization error: {0}")]
52    Serialization(String),
53
54    /// Cryptographic error.
55    #[error("crypto error: {0}")]
56    Crypto(String),
57
58    /// I/O error.
59    #[error("I/O error: {0}")]
60    Io(#[from] std::io::Error),
61
62    /// Configuration error.
63    #[error("configuration error: {0}")]
64    Config(String),
65
66    /// Timeout waiting for a response.
67    #[error("timeout: {0}")]
68    Timeout(String),
69
70    /// Insufficient peers for the operation.
71    #[error("insufficient peers: {0}")]
72    InsufficientPeers(String),
73
74    /// BLS signature verification failed.
75    #[error("signature verification failed: {0}")]
76    SignatureVerification(String),
77
78    /// Self-encryption operation failed.
79    #[error("encryption error: {0}")]
80    Encryption(String),
81
82    /// The operation was cancelled by the caller rather than failing.
83    ///
84    /// Returned, for example, by streaming downloads when the consumer drops
85    /// its receiver (a client disconnect) — distinct from a transport
86    /// [`Error::Network`] failure, since nothing went wrong on the wire.
87    #[error("operation cancelled: {0}")]
88    Cancelled(String),
89
90    /// Data already exists on the network — no payment needed.
91    #[error("already stored on network")]
92    AlreadyStored,
93
94    /// A peer's quote `pub_key` does not BLAKE3-hash to the peer ID. The
95    /// storer would reject any `ProofOfPayment` containing this quote, so
96    /// the client drops the response before payment.
97    #[error("bad quote binding from peer {peer_id}: {detail}")]
98    BadQuoteBinding {
99        /// The peer ID we got the quote from (claimed identity).
100        peer_id: String,
101        /// Diagnostic detail (e.g. "BLAKE3(pub_key) = …, peer_id = …").
102        detail: String,
103    },
104
105    /// Not enough disk space for the operation.
106    #[error("insufficient disk space: {0}")]
107    InsufficientDiskSpace(String),
108
109    /// Cost estimation could not reach a representative quote.
110    ///
111    /// Returned by [`crate::data::Client::estimate_upload_cost`] when every
112    /// sampled chunk address reported `AlreadyStored`, so the network price
113    /// for the remainder of the file cannot be inferred from a sample.
114    /// The attached message describes how many addresses were tried.
115    #[error("cost estimation inconclusive: {0}")]
116    CostEstimationInconclusive(String),
117
118    /// Upload partially succeeded -- some chunks stored, some failed after retries.
119    ///
120    /// The `stored` addresses can be used for progress tracking and resume.
121    #[error(
122        "partial upload: {stored_count}/{total_chunks} stored, {failed_count} failed: {reason}"
123    )]
124    PartialUpload {
125        /// Addresses of successfully stored chunks.
126        stored: Vec<[u8; 32]>,
127        /// Number of successfully stored chunks.
128        stored_count: usize,
129        /// Addresses and error messages of chunks that failed after retries.
130        failed: Vec<([u8; 32], String)>,
131        /// Number of failed chunks.
132        failed_count: usize,
133        /// Total number of chunks the upload was attempting to store.
134        total_chunks: usize,
135        /// On-chain spend incurred so far. Boxed to keep the `Error` enum small
136        /// (the variant is returned in `Result` across the crate; without the
137        /// box the two cost fields would trip `clippy::result_large_err`).
138        spend: Box<PartialUploadSpend>,
139        /// Root cause description.
140        reason: String,
141    },
142}
143
144/// On-chain spend recorded on a [`Error::PartialUpload`].
145///
146/// A partial upload still spends money for the chunks it paid for. In the
147/// single-node path payment precedes store, so this includes a failed wave's
148/// chunks; surfacing it lets the caller report real spend rather than silently
149/// dropping it.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct PartialUploadSpend {
152    /// Storage cost paid on-chain so far, in atto-tokens.
153    pub storage_cost_atto: String,
154    /// Gas cost paid on-chain so far, in wei.
155    pub gas_cost_wei: u128,
156}
157
158// ant-node is only linked when the `devnet` feature is on, so the
159// blanket `From` impl follows that gate. LocalDevnet maps node errors
160// to `Error::Network` via this conversion; default builds never see it.
161#[cfg(feature = "devnet")]
162impl From<ant_node::Error> for Error {
163    fn from(e: ant_node::Error) -> Self {
164        Self::Network(e.to_string())
165    }
166}
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used, clippy::expect_used)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_display_network() {
175        let err = Error::Network("connection refused".to_string());
176        assert_eq!(err.to_string(), "network error: connection refused");
177    }
178
179    #[test]
180    fn test_display_storage() {
181        let err = Error::Storage("disk full".to_string());
182        assert_eq!(err.to_string(), "storage error: disk full");
183    }
184
185    #[test]
186    fn test_display_payment() {
187        let err = Error::Payment("insufficient funds".to_string());
188        assert_eq!(err.to_string(), "payment error: insufficient funds");
189    }
190
191    #[test]
192    fn test_display_protocol() {
193        let err = Error::Protocol("invalid message".to_string());
194        assert_eq!(err.to_string(), "protocol error: invalid message");
195    }
196
197    #[test]
198    fn test_display_invalid_data() {
199        let err = Error::InvalidData("bad hash".to_string());
200        assert_eq!(err.to_string(), "invalid data: bad hash");
201    }
202
203    #[test]
204    fn test_display_serialization() {
205        let err = Error::Serialization("decode failed".to_string());
206        assert_eq!(err.to_string(), "serialization error: decode failed");
207    }
208
209    #[test]
210    fn test_display_crypto() {
211        let err = Error::Crypto("key mismatch".to_string());
212        assert_eq!(err.to_string(), "crypto error: key mismatch");
213    }
214
215    #[test]
216    fn test_display_io() {
217        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
218        let err = Error::Io(io_err);
219        assert_eq!(err.to_string(), "I/O error: file missing");
220    }
221
222    #[test]
223    fn test_display_config() {
224        let err = Error::Config("bad value".to_string());
225        assert_eq!(err.to_string(), "configuration error: bad value");
226    }
227
228    #[test]
229    fn test_display_timeout() {
230        let err = Error::Timeout("30s elapsed".to_string());
231        assert_eq!(err.to_string(), "timeout: 30s elapsed");
232    }
233
234    #[test]
235    fn test_display_insufficient_peers() {
236        let err = Error::InsufficientPeers("need 5, got 2".to_string());
237        assert_eq!(err.to_string(), "insufficient peers: need 5, got 2");
238    }
239
240    #[test]
241    fn test_display_signature_verification() {
242        let err = Error::SignatureVerification("invalid sig".to_string());
243        assert_eq!(
244            err.to_string(),
245            "signature verification failed: invalid sig"
246        );
247    }
248
249    #[test]
250    fn test_display_encryption() {
251        let err = Error::Encryption("decrypt failed".to_string());
252        assert_eq!(err.to_string(), "encryption error: decrypt failed");
253    }
254
255    #[test]
256    fn test_display_cancelled() {
257        let err = Error::Cancelled("download stream receiver dropped".to_string());
258        assert_eq!(
259            err.to_string(),
260            "operation cancelled: download stream receiver dropped"
261        );
262    }
263
264    #[test]
265    fn test_display_insufficient_disk_space() {
266        let err = Error::InsufficientDiskSpace("need 100 MB but only 10 MB available".to_string());
267        assert_eq!(
268            err.to_string(),
269            "insufficient disk space: need 100 MB but only 10 MB available"
270        );
271    }
272
273    #[test]
274    fn test_display_cost_estimation_inconclusive() {
275        let err = Error::CostEstimationInconclusive(
276            "sampled 5 addresses, all already stored".to_string(),
277        );
278        assert_eq!(
279            err.to_string(),
280            "cost estimation inconclusive: sampled 5 addresses, all already stored"
281        );
282    }
283
284    #[test]
285    fn test_from_io_error() {
286        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
287        let err: Error = io_err.into();
288        assert!(matches!(err, Error::Io(_)));
289    }
290}