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