Skip to main content

kinetic_core/error/
dht.rs

1//! DHT record rejection, resolution, publish, and registration error types.
2//!
3//! Defines three primary error enums used in the two-phase name registration
4//! protocol (commit → reveal) and the DHT name resolution flow:
5//!
6//! - [`RecordRejectReason`] — fine-grained reasons a DHT `PUT` was rejected by
7//!   the local `KineticRecordStore`.
8//! - [`ResolutionError`] — errors during DHT name lookup (`KIN-RES-NNN`).
9//! - [`PublishError`] — errors when pushing records to the DHT (`KIN-PUB-NNN`).
10//! - [`RegistrationError`] — errors in the full name registration flow (`KIN-REG-NNN`).
11//!
12//! All three rich error types expose `code()`, `error_type_uri()`, `is_retryable()`,
13//! `severity()`, `user_message()`, and `details()` to satisfy the Kinetic error taxonomy.
14use super::vdf::VdfRejectReason;
15use super::Severity;
16use thiserror::Error;
17
18/// Why a DHT record was rejected by the local store.
19#[derive(Error, Debug, PartialEq, Eq)]
20pub enum RecordRejectReason {
21    /// The record's Ed25519 signature did not verify against the public key.
22    #[error("invalid signature")]
23    InvalidSignature,
24    /// The embedded VDF proof failed verification.
25    #[error("VDF proof invalid")]
26    InvalidVdf,
27    /// The registration epoch has passed and the record is no longer valid.
28    #[error("registration has expired")]
29    Expired,
30    /// The name is already owned by a different public key.
31    #[error("name already owned by a different key")]
32    AlreadyOwned,
33
34    /// The VDF iteration count is below the minimum required for this name and kyn.
35    #[error("insufficient VDF iterations to claim ownership")]
36    InsufficientIterations,
37    /// The record lost an XOR-distance tie-break to a competing record.
38    #[error("lost XOR tie-break to stronger record")]
39    TieBroken,
40    /// The revealed data's hash does not match the stored commitment.
41    #[error("commitment mismatch")]
42    CommitmentMismatch,
43    /// The `drand_signature` field contains non-hex characters.
44    #[error("drand_signature contains invalid hex")]
45    InvalidDrandHex,
46    /// The public key bytes could not be parsed as a valid Ed25519 key.
47    #[error("public key bytes are malformed")]
48    InvalidPublicKey,
49    /// The signature bytes are not 64 bytes long or are otherwise malformed.
50    #[error("signature bytes are malformed")]
51    MalformedSignature,
52}
53
54// ─── ResolutionError ──────────────────────────────────────────────────────────
55
56/// Errors during DHT name resolution. Rich developer context, NOT serialized over wire.
57/// Convert to `ApiError` at the HTTP/FFI boundary.
58#[derive(Error, Debug)]
59pub enum ResolutionError {
60    /// The local node has no connected peers and cannot reach the DHT.
61    #[error("Node is offline — no peers connected")]
62    Offline,
63    /// The name was not found after querying the given number of peers.
64    #[error("'{name}' not found after querying {peers_queried} peers")]
65    NotFound {
66        /// The `.kin` name that was queried.
67        name: String,
68        /// Number of DHT peers that were contacted.
69        peers_queried: usize,
70    },
71    /// The name was found but one or more of the returned records failed VDF verification.
72    #[error("'{name}' found but {count} record(s) failed VDF verification")]
73    VdfVerificationFailed {
74        /// The `.kin` name that was queried.
75        name: String,
76        /// Number of records that failed verification.
77        count: usize,
78    },
79    /// The name's registration has passed its validity window.
80    #[error("'{name}' registration has expired ({age} rounds old)")]
81    Expired {
82        /// The `.kin` name that was queried.
83        name: String,
84        /// Age of the record in drand rounds.
85        age: u64,
86    },
87    /// The resolution attempt timed out before a result was returned.
88    #[error("Resolution timed out after {elapsed_ms}ms ({peers_queried} peers queried)")]
89    Timeout {
90        /// The `.kin` name that was queried.
91        name: String,
92        /// Wall-clock time elapsed during the query in milliseconds.
93        elapsed_ms: u64,
94        /// Number of DHT peers that were contacted before the timeout.
95        peers_queried: usize,
96    },
97    /// An unexpected internal error occurred during resolution.
98    #[error("Internal error: {message}")]
99    Internal {
100        /// Developer-facing description of what went wrong.
101        message: String,
102        /// Optional chain of underlying error causes.
103        #[source]
104        source: Option<Box<dyn std::error::Error + Send + Sync>>,
105    },
106}
107
108impl ResolutionError {
109    /// Stable protocol error code. Part of the Kinetic error taxonomy.
110    pub fn code(&self) -> &'static str {
111        match self {
112            Self::Offline => "KIN-RES-001",
113            Self::NotFound { .. } => "KIN-RES-002",
114            Self::VdfVerificationFailed { .. } => "KIN-RES-003",
115            Self::Expired { .. } => "KIN-RES-004",
116            Self::Timeout { .. } => "KIN-RES-005",
117            Self::Internal { .. } => "KIN-RES-006",
118        }
119    }
120
121    /// RFC 7807 type URI for this error.
122    pub fn error_type_uri(&self) -> String {
123        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
124    }
125
126    /// Whether the client should offer a retry action.
127    pub fn is_retryable(&self) -> bool {
128        matches!(self, Self::Offline | Self::Timeout { .. })
129    }
130
131    /// Severity level for logging and monitoring.
132    pub fn severity(&self) -> Severity {
133        match self {
134            Self::Offline => Severity::Warning,
135            Self::NotFound { .. } => Severity::Info,
136            Self::VdfVerificationFailed { .. } => Severity::Error,
137            Self::Expired { .. } => Severity::Info,
138            Self::Timeout { .. } => Severity::Warning,
139            Self::Internal { .. } => Severity::Error,
140        }
141    }
142
143    /// Clean user-facing message with no developer details.
144    pub fn user_message(&self) -> String {
145        match self {
146            Self::Offline => {
147                "You appear to be offline. Check your internet connection.".to_string()
148            }
149            Self::NotFound { name, .. } => {
150                format!("'{}' is not registered on the Kinetic network.", name)
151            }
152            Self::VdfVerificationFailed { name, .. } => format!(
153                "'{}' has an invalid cryptographic proof. This record may have been tampered with.",
154                name
155            ),
156            Self::Expired { name, .. } => format!(
157                "'{}' registration has expired. The owner needs to renew it.",
158                name
159            ),
160            Self::Timeout { name, .. } => format!(
161                "The network took too long to respond for '{}'. Please try again.",
162                name
163            ),
164            Self::Internal { .. } => {
165                "An internal network error occurred. Please try again.".to_string()
166            }
167        }
168    }
169
170    /// Structured developer-facing details for ApiError.details.
171    pub fn details(&self) -> serde_json::Value {
172        match self {
173            Self::NotFound { peers_queried, .. } => {
174                serde_json::json!({ "peers_queried": peers_queried })
175            }
176            Self::Timeout {
177                elapsed_ms,
178                peers_queried,
179                ..
180            } => serde_json::json!({ "elapsed_ms": elapsed_ms, "peers_queried": peers_queried }),
181            Self::VdfVerificationFailed { count, .. } => {
182                serde_json::json!({ "failed_record_count": count })
183            }
184            Self::Expired { age, .. } => serde_json::json!({ "age_rounds": age }),
185            _ => serde_json::Value::Null,
186        }
187    }
188}
189
190// ─── PublishError ─────────────────────────────────────────────────────────────
191
192/// Errors when publishing records to the DHT.
193#[derive(Error, Debug)]
194pub enum PublishError {
195    /// The local node has no connected peers and cannot write to the DHT.
196    #[error("Node is offline — cannot publish to the DHT")]
197    Offline,
198    /// The VDF proof attached to the record failed verification.
199    #[error("VDF proof is invalid: {0}")]
200    InvalidProof(#[from] VdfRejectReason),
201    /// The name is already owned by a different Ed25519 public key.
202    #[error("'{name}' is already owned by a different key")]
203    AlreadyOwned {
204        /// The `.kin` name that is already registered.
205        name: String,
206    },
207    /// Every DHT `PUT` attempt for this record failed.
208    #[error("All {count} DHT put operations failed")]
209    AllFailed {
210        /// Number of failed PUT operations.
211        count: usize,
212    },
213    /// The record was rejected by the store (e.g. invalid signature, stale).
214    #[error("Rejected by the network: {0}")]
215    Rejected(String),
216    /// An unexpected internal error occurred during the publish flow.
217    #[error("Internal error: {message}")]
218    Internal {
219        /// Developer-facing description of what went wrong.
220        message: String,
221        /// Optional chain of underlying error causes.
222        #[source]
223        source: Option<Box<dyn std::error::Error + Send + Sync>>,
224    },
225}
226
227impl PublishError {
228    /// Stable protocol error code. Part of the Kinetic error taxonomy.
229    pub fn code(&self) -> &'static str {
230        match self {
231            Self::Offline => "KIN-PUB-001",
232            Self::InvalidProof(_) => "KIN-PUB-002",
233            Self::AlreadyOwned { .. } => "KIN-PUB-003",
234            Self::AllFailed { .. } => "KIN-PUB-004",
235            Self::Rejected(_) => "KIN-PUB-005",
236            Self::Internal { .. } => "KIN-PUB-006",
237        }
238    }
239
240    /// RFC 7807 type URI for this error.
241    pub fn error_type_uri(&self) -> String {
242        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
243    }
244
245    /// Whether the client should offer a retry action.
246    pub fn is_retryable(&self) -> bool {
247        matches!(self, Self::Offline | Self::AllFailed { .. })
248    }
249
250    /// Severity level for logging and monitoring.
251    pub fn severity(&self) -> Severity {
252        match self {
253            Self::Offline => Severity::Warning,
254            Self::InvalidProof(_) => Severity::Error,
255            Self::AlreadyOwned { .. } => Severity::Info,
256            Self::AllFailed { .. } => Severity::Warning,
257            Self::Rejected(_) => Severity::Warning,
258            Self::Internal { .. } => Severity::Error,
259        }
260    }
261
262    /// Clean user-facing message with no developer details.
263    pub fn user_message(&self) -> String {
264        match self {
265            Self::Offline => "You appear to be offline. Cannot publish to the network.".to_string(),
266            Self::InvalidProof(_) => "The VDF proof is invalid and was rejected.".to_string(),
267            Self::AlreadyOwned { name } => {
268                format!("'{}' is already registered under a different key.", name)
269            }
270            Self::AllFailed { .. } => {
271                "The network rejected all publish attempts. Please try again.".to_string()
272            }
273            Self::Rejected(reason) => format!("Publish rejected: {}", reason),
274            Self::Internal { .. } => "An internal error occurred during publishing.".to_string(),
275        }
276    }
277
278    /// Structured developer-facing details for [`ApiError`](crate::api_error::ApiError).
279    pub fn details(&self) -> serde_json::Value {
280        match self {
281            Self::AllFailed { count } => serde_json::json!({ "failed_count": count }),
282            Self::InvalidProof(r) => serde_json::json!({ "reason": r.to_string() }),
283            _ => serde_json::Value::Null,
284        }
285    }
286}
287
288// ─── RegistrationError ────────────────────────────────────────────────────────
289
290/// Errors during .kin name registration flow.
291#[derive(Error, Debug)]
292pub enum RegistrationError {
293    /// The requested name contains characters not allowed by the Kinetic naming rules.
294    #[error("Name '{name}' contains invalid characters")]
295    InvalidName {
296        /// The invalid name that was submitted.
297        name: String,
298    },
299    /// The VDF computation step failed (e.g. chiavdf returned an error).
300    #[error("VDF computation failed: {0}")]
301    VdfFailed(#[from] VdfRejectReason),
302    /// The revealed data's hash did not match the previously published commitment.
303    #[error("Commitment mismatch — reveal data does not match commitment")]
304    CommitmentMismatch,
305    /// The name was claimed by a different key before this registration completed.
306    #[error("'{name}' is already owned by a different key")]
307    AlreadyOwned {
308        /// The `.kin` name that is already registered.
309        name: String,
310    },
311    /// A VDF task for this name is already running; only one at a time is permitted.
312    #[error("A VDF registration is already in progress for '{name}'")]
313    AlreadyInProgress {
314        /// The `.kin` name whose registration is already running.
315        name: String,
316    },
317    /// The network rejected the registration record for the stated reason.
318    #[error("Registration rejected by the network: {reason}")]
319    NetworkRejected {
320        /// The specific reason the record was rejected.
321        reason: RecordRejectReason,
322    },
323    /// An unexpected internal error occurred during the registration flow.
324    #[error("Internal error: {message}")]
325    Internal {
326        /// Developer-facing description of what went wrong.
327        message: String,
328        /// Optional chain of underlying error causes.
329        #[source]
330        source: Option<Box<dyn std::error::Error + Send + Sync>>,
331    },
332}
333
334impl RegistrationError {
335    /// Stable protocol error code. Part of the Kinetic error taxonomy.
336    pub fn code(&self) -> &'static str {
337        match self {
338            Self::InvalidName { .. } => "KIN-REG-001",
339            Self::VdfFailed(_) => "KIN-REG-002",
340            Self::CommitmentMismatch => "KIN-REG-003",
341            Self::AlreadyOwned { .. } => "KIN-REG-004",
342            Self::AlreadyInProgress { .. } => "KIN-REG-005",
343            Self::NetworkRejected { .. } => "KIN-REG-006",
344            Self::Internal { .. } => "KIN-REG-007",
345        }
346    }
347
348    /// RFC 7807 type URI for this error.
349    pub fn error_type_uri(&self) -> String {
350        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
351    }
352
353    /// Whether the client should offer a retry action.
354    pub fn is_retryable(&self) -> bool {
355        matches!(self, Self::VdfFailed(_))
356    }
357
358    /// Severity level for logging and monitoring.
359    pub fn severity(&self) -> Severity {
360        match self {
361            Self::InvalidName { .. } => Severity::Info,
362            Self::VdfFailed(_) => Severity::Error,
363            Self::CommitmentMismatch => Severity::Error,
364            Self::AlreadyOwned { .. } => Severity::Info,
365            Self::AlreadyInProgress { .. } => Severity::Info,
366            Self::NetworkRejected { .. } => Severity::Warning,
367            Self::Internal { .. } => Severity::Error,
368        }
369    }
370
371    /// Clean user-facing message with no developer details.
372    pub fn user_message(&self) -> String {
373        match self {
374            Self::InvalidName { name } => format!("'{}' contains invalid characters. Use only lowercase letters, digits, and hyphens.", name),
375            Self::VdfFailed(_) => "The VDF computation failed. Please try again.".to_string(),
376            Self::CommitmentMismatch => "The registration data is inconsistent. Please restart the registration process.".to_string(),
377            Self::AlreadyOwned { name } => format!("'{}' is already registered by someone else.", name),
378            Self::AlreadyInProgress { name } => format!("A registration is already in progress for '{}'.", name),
379            Self::NetworkRejected { reason } => format!("Registration was rejected: {}", reason),
380            Self::Internal { .. } => "An internal error occurred during registration.".to_string(),
381        }
382    }
383
384    /// Structured developer-facing details for [`ApiError`](crate::api_error::ApiError).
385    pub fn details(&self) -> serde_json::Value {
386        match self {
387            Self::NetworkRejected { reason } => {
388                serde_json::json!({ "reject_reason": reason.to_string() })
389            }
390            _ => serde_json::Value::Null,
391        }
392    }
393}
394
395impl PartialEq for ResolutionError {
396    fn eq(&self, other: &Self) -> bool {
397        match (self, other) {
398            (Self::Offline, Self::Offline) => true,
399            (
400                Self::NotFound {
401                    name: a_n,
402                    peers_queried: a_p,
403                },
404                Self::NotFound {
405                    name: b_n,
406                    peers_queried: b_p,
407                },
408            ) => a_n == b_n && a_p == b_p,
409            (
410                Self::VdfVerificationFailed {
411                    name: a_n,
412                    count: a_c,
413                },
414                Self::VdfVerificationFailed {
415                    name: b_n,
416                    count: b_c,
417                },
418            ) => a_n == b_n && a_c == b_c,
419            (
420                Self::Expired {
421                    name: a_n,
422                    age: a_a,
423                },
424                Self::Expired {
425                    name: b_n,
426                    age: b_a,
427                },
428            ) => a_n == b_n && a_a == b_a,
429            (
430                Self::Timeout {
431                    name: a_n,
432                    elapsed_ms: a_e,
433                    peers_queried: a_p,
434                },
435                Self::Timeout {
436                    name: b_n,
437                    elapsed_ms: b_e,
438                    peers_queried: b_p,
439                },
440            ) => a_n == b_n && a_e == b_e && a_p == b_p,
441            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
442                a_m == b_m
443            }
444            _ => false,
445        }
446    }
447}
448impl Eq for ResolutionError {}
449
450impl PartialEq for PublishError {
451    fn eq(&self, other: &Self) -> bool {
452        match (self, other) {
453            (Self::Offline, Self::Offline) => true,
454            (Self::InvalidProof(a), Self::InvalidProof(b)) => a == b,
455            (Self::AlreadyOwned { name: a_n }, Self::AlreadyOwned { name: b_n }) => a_n == b_n,
456            (Self::AllFailed { count: a_c }, Self::AllFailed { count: b_c }) => a_c == b_c,
457            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
458                a_m == b_m
459            }
460            _ => false,
461        }
462    }
463}
464impl Eq for PublishError {}
465
466impl PartialEq for RegistrationError {
467    fn eq(&self, other: &Self) -> bool {
468        match (self, other) {
469            (Self::InvalidName { name: a_n }, Self::InvalidName { name: b_n }) => a_n == b_n,
470            (Self::VdfFailed(a), Self::VdfFailed(b)) => a == b,
471            (Self::CommitmentMismatch, Self::CommitmentMismatch) => true,
472            (Self::AlreadyOwned { name: a_n }, Self::AlreadyOwned { name: b_n }) => a_n == b_n,
473            (Self::AlreadyInProgress { name: a_n }, Self::AlreadyInProgress { name: b_n }) => {
474                a_n == b_n
475            }
476            (Self::NetworkRejected { reason: a_r }, Self::NetworkRejected { reason: b_r }) => {
477                a_r == b_r
478            }
479            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
480                a_m == b_m
481            }
482            _ => false,
483        }
484    }
485}
486impl Eq for RegistrationError {}