kinetic-core 0.2.0

Core daemon primitives, VDF management, and network utilities for the Kinetic Network.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
//! DHT record rejection, resolution, publish, and registration error types.
//!
//! Defines three primary error enums used in the two-phase name registration
//! protocol (commit → reveal) and the DHT name resolution flow:
//!
//! - [`RecordRejectReason`] — fine-grained reasons a DHT `PUT` was rejected by
//!   the local `KineticRecordStore`.
//! - [`ResolutionError`] — errors during DHT name lookup (`KIN-RES-NNN`).
//! - [`PublishError`] — errors when pushing records to the DHT (`KIN-PUB-NNN`).
//! - [`RegistrationError`] — errors in the full name registration flow (`KIN-REG-NNN`).
//!
//! All three rich error types expose `code()`, `error_type_uri()`, `is_retryable()`,
//! `severity()`, `user_message()`, and `details()` to satisfy the Kinetic error taxonomy.
use super::vdf::VdfRejectReason;
use super::Severity;
use thiserror::Error;

/// Why a DHT record was rejected by the local store.
#[derive(Error, Debug, PartialEq, Eq)]
pub enum RecordRejectReason {
    /// The record's Ed25519 signature did not verify against the public key.
    #[error("invalid signature")]
    InvalidSignature,
    /// The embedded VDF proof failed verification.
    #[error("VDF proof invalid")]
    InvalidVdf,
    /// The registration epoch has passed and the record is no longer valid.
    #[error("registration has expired")]
    Expired,
    /// The name is already owned by a different public key.
    #[error("name already owned by a different key")]
    AlreadyOwned,

    /// The VDF iteration count is below the minimum required for this name and kyn.
    #[error("insufficient VDF iterations to claim ownership")]
    InsufficientIterations,
    /// The record lost an XOR-distance tie-break to a competing record.
    #[error("lost XOR tie-break to stronger record")]
    TieBroken,
    /// The revealed data's hash does not match the stored commitment.
    #[error("commitment mismatch")]
    CommitmentMismatch,
    /// The `drand_signature` field contains non-hex characters.
    #[error("drand_signature contains invalid hex")]
    InvalidDrandHex,
    /// The public key bytes could not be parsed as a valid Ed25519 key.
    #[error("public key bytes are malformed")]
    InvalidPublicKey,
    /// The signature bytes are not 64 bytes long or are otherwise malformed.
    #[error("signature bytes are malformed")]
    MalformedSignature,
}

// ─── ResolutionError ──────────────────────────────────────────────────────────

/// Errors during DHT name resolution. Rich developer context, NOT serialized over wire.
/// Convert to `ApiError` at the HTTP/FFI boundary.
#[derive(Error, Debug)]
pub enum ResolutionError {
    /// The local node has no connected peers and cannot reach the DHT.
    #[error("Node is offline — no peers connected")]
    Offline,
    /// The name was not found after querying the given number of peers.
    #[error("'{name}' not found after querying {peers_queried} peers")]
    NotFound {
        /// The `.kin` name that was queried.
        name: String,
        /// Number of DHT peers that were contacted.
        peers_queried: usize,
    },
    /// The name was found but one or more of the returned records failed VDF verification.
    #[error("'{name}' found but {count} record(s) failed VDF verification")]
    VdfVerificationFailed {
        /// The `.kin` name that was queried.
        name: String,
        /// Number of records that failed verification.
        count: usize,
    },
    /// The name's registration has passed its validity window.
    #[error("'{name}' registration has expired ({age} rounds old)")]
    Expired {
        /// The `.kin` name that was queried.
        name: String,
        /// Age of the record in drand rounds.
        age: u64,
    },
    /// The resolution attempt timed out before a result was returned.
    #[error("Resolution timed out after {elapsed_ms}ms ({peers_queried} peers queried)")]
    Timeout {
        /// The `.kin` name that was queried.
        name: String,
        /// Wall-clock time elapsed during the query in milliseconds.
        elapsed_ms: u64,
        /// Number of DHT peers that were contacted before the timeout.
        peers_queried: usize,
    },
    /// An unexpected internal error occurred during resolution.
    #[error("Internal error: {message}")]
    Internal {
        /// Developer-facing description of what went wrong.
        message: String,
        /// Optional chain of underlying error causes.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
}

impl ResolutionError {
    /// Stable protocol error code. Part of the Kinetic error taxonomy.
    pub fn code(&self) -> &'static str {
        match self {
            Self::Offline => "KIN-RES-001",
            Self::NotFound { .. } => "KIN-RES-002",
            Self::VdfVerificationFailed { .. } => "KIN-RES-003",
            Self::Expired { .. } => "KIN-RES-004",
            Self::Timeout { .. } => "KIN-RES-005",
            Self::Internal { .. } => "KIN-RES-006",
        }
    }

    /// RFC 7807 type URI for this error.
    pub fn error_type_uri(&self) -> String {
        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
    }

    /// Whether the client should offer a retry action.
    pub fn is_retryable(&self) -> bool {
        matches!(self, Self::Offline | Self::Timeout { .. })
    }

    /// Severity level for logging and monitoring.
    pub fn severity(&self) -> Severity {
        match self {
            Self::Offline => Severity::Warning,
            Self::NotFound { .. } => Severity::Info,
            Self::VdfVerificationFailed { .. } => Severity::Error,
            Self::Expired { .. } => Severity::Info,
            Self::Timeout { .. } => Severity::Warning,
            Self::Internal { .. } => Severity::Error,
        }
    }

    /// Clean user-facing message with no developer details.
    pub fn user_message(&self) -> String {
        match self {
            Self::Offline => {
                "You appear to be offline. Check your internet connection.".to_string()
            }
            Self::NotFound { name, .. } => {
                format!("'{}' is not registered on the Kinetic network.", name)
            }
            Self::VdfVerificationFailed { name, .. } => format!(
                "'{}' has an invalid cryptographic proof. This record may have been tampered with.",
                name
            ),
            Self::Expired { name, .. } => format!(
                "'{}' registration has expired. The owner needs to renew it.",
                name
            ),
            Self::Timeout { name, .. } => format!(
                "The network took too long to respond for '{}'. Please try again.",
                name
            ),
            Self::Internal { .. } => {
                "An internal network error occurred. Please try again.".to_string()
            }
        }
    }

    /// Structured developer-facing details for ApiError.details.
    pub fn details(&self) -> serde_json::Value {
        match self {
            Self::NotFound { peers_queried, .. } => {
                serde_json::json!({ "peers_queried": peers_queried })
            }
            Self::Timeout {
                elapsed_ms,
                peers_queried,
                ..
            } => serde_json::json!({ "elapsed_ms": elapsed_ms, "peers_queried": peers_queried }),
            Self::VdfVerificationFailed { count, .. } => {
                serde_json::json!({ "failed_record_count": count })
            }
            Self::Expired { age, .. } => serde_json::json!({ "age_rounds": age }),
            _ => serde_json::Value::Null,
        }
    }
}

// ─── PublishError ─────────────────────────────────────────────────────────────

/// Errors when publishing records to the DHT.
#[derive(Error, Debug)]
pub enum PublishError {
    /// The local node has no connected peers and cannot write to the DHT.
    #[error("Node is offline — cannot publish to the DHT")]
    Offline,
    /// The VDF proof attached to the record failed verification.
    #[error("VDF proof is invalid: {0}")]
    InvalidProof(#[from] VdfRejectReason),
    /// The name is already owned by a different Ed25519 public key.
    #[error("'{name}' is already owned by a different key")]
    AlreadyOwned {
        /// The `.kin` name that is already registered.
        name: String,
    },
    /// Every DHT `PUT` attempt for this record failed.
    #[error("All {count} DHT put operations failed")]
    AllFailed {
        /// Number of failed PUT operations.
        count: usize,
    },
    /// The record was rejected by the store (e.g. invalid signature, stale).
    #[error("Rejected by the network: {0}")]
    Rejected(String),
    /// An unexpected internal error occurred during the publish flow.
    #[error("Internal error: {message}")]
    Internal {
        /// Developer-facing description of what went wrong.
        message: String,
        /// Optional chain of underlying error causes.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
}

impl PublishError {
    /// Stable protocol error code. Part of the Kinetic error taxonomy.
    pub fn code(&self) -> &'static str {
        match self {
            Self::Offline => "KIN-PUB-001",
            Self::InvalidProof(_) => "KIN-PUB-002",
            Self::AlreadyOwned { .. } => "KIN-PUB-003",
            Self::AllFailed { .. } => "KIN-PUB-004",
            Self::Rejected(_) => "KIN-PUB-005",
            Self::Internal { .. } => "KIN-PUB-006",
        }
    }

    /// RFC 7807 type URI for this error.
    pub fn error_type_uri(&self) -> String {
        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
    }

    /// Whether the client should offer a retry action.
    pub fn is_retryable(&self) -> bool {
        matches!(self, Self::Offline | Self::AllFailed { .. })
    }

    /// Severity level for logging and monitoring.
    pub fn severity(&self) -> Severity {
        match self {
            Self::Offline => Severity::Warning,
            Self::InvalidProof(_) => Severity::Error,
            Self::AlreadyOwned { .. } => Severity::Info,
            Self::AllFailed { .. } => Severity::Warning,
            Self::Rejected(_) => Severity::Warning,
            Self::Internal { .. } => Severity::Error,
        }
    }

    /// Clean user-facing message with no developer details.
    pub fn user_message(&self) -> String {
        match self {
            Self::Offline => "You appear to be offline. Cannot publish to the network.".to_string(),
            Self::InvalidProof(_) => "The VDF proof is invalid and was rejected.".to_string(),
            Self::AlreadyOwned { name } => {
                format!("'{}' is already registered under a different key.", name)
            }
            Self::AllFailed { .. } => {
                "The network rejected all publish attempts. Please try again.".to_string()
            }
            Self::Rejected(reason) => format!("Publish rejected: {}", reason),
            Self::Internal { .. } => "An internal error occurred during publishing.".to_string(),
        }
    }

    /// Structured developer-facing details for [`ApiError`](crate::api_error::ApiError).
    pub fn details(&self) -> serde_json::Value {
        match self {
            Self::AllFailed { count } => serde_json::json!({ "failed_count": count }),
            Self::InvalidProof(r) => serde_json::json!({ "reason": r.to_string() }),
            _ => serde_json::Value::Null,
        }
    }
}

// ─── RegistrationError ────────────────────────────────────────────────────────

/// Errors during .kin name registration flow.
#[derive(Error, Debug)]
pub enum RegistrationError {
    /// The requested name contains characters not allowed by the Kinetic naming rules.
    #[error("Name '{name}' contains invalid characters")]
    InvalidName {
        /// The invalid name that was submitted.
        name: String,
    },
    /// The VDF computation step failed (e.g. chiavdf returned an error).
    #[error("VDF computation failed: {0}")]
    VdfFailed(#[from] VdfRejectReason),
    /// The revealed data's hash did not match the previously published commitment.
    #[error("Commitment mismatch — reveal data does not match commitment")]
    CommitmentMismatch,
    /// The name was claimed by a different key before this registration completed.
    #[error("'{name}' is already owned by a different key")]
    AlreadyOwned {
        /// The `.kin` name that is already registered.
        name: String,
    },
    /// A VDF task for this name is already running; only one at a time is permitted.
    #[error("A VDF registration is already in progress for '{name}'")]
    AlreadyInProgress {
        /// The `.kin` name whose registration is already running.
        name: String,
    },
    /// The network rejected the registration record for the stated reason.
    #[error("Registration rejected by the network: {reason}")]
    NetworkRejected {
        /// The specific reason the record was rejected.
        reason: RecordRejectReason,
    },
    /// An unexpected internal error occurred during the registration flow.
    #[error("Internal error: {message}")]
    Internal {
        /// Developer-facing description of what went wrong.
        message: String,
        /// Optional chain of underlying error causes.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
}

impl RegistrationError {
    /// Stable protocol error code. Part of the Kinetic error taxonomy.
    pub fn code(&self) -> &'static str {
        match self {
            Self::InvalidName { .. } => "KIN-REG-001",
            Self::VdfFailed(_) => "KIN-REG-002",
            Self::CommitmentMismatch => "KIN-REG-003",
            Self::AlreadyOwned { .. } => "KIN-REG-004",
            Self::AlreadyInProgress { .. } => "KIN-REG-005",
            Self::NetworkRejected { .. } => "KIN-REG-006",
            Self::Internal { .. } => "KIN-REG-007",
        }
    }

    /// RFC 7807 type URI for this error.
    pub fn error_type_uri(&self) -> String {
        format!("{}/errors/{}", crate::constants::DOCS_URL, self.code())
    }

    /// Whether the client should offer a retry action.
    pub fn is_retryable(&self) -> bool {
        matches!(self, Self::VdfFailed(_))
    }

    /// Severity level for logging and monitoring.
    pub fn severity(&self) -> Severity {
        match self {
            Self::InvalidName { .. } => Severity::Info,
            Self::VdfFailed(_) => Severity::Error,
            Self::CommitmentMismatch => Severity::Error,
            Self::AlreadyOwned { .. } => Severity::Info,
            Self::AlreadyInProgress { .. } => Severity::Info,
            Self::NetworkRejected { .. } => Severity::Warning,
            Self::Internal { .. } => Severity::Error,
        }
    }

    /// Clean user-facing message with no developer details.
    pub fn user_message(&self) -> String {
        match self {
            Self::InvalidName { name } => format!("'{}' contains invalid characters. Use only lowercase letters, digits, and hyphens.", name),
            Self::VdfFailed(_) => "The VDF computation failed. Please try again.".to_string(),
            Self::CommitmentMismatch => "The registration data is inconsistent. Please restart the registration process.".to_string(),
            Self::AlreadyOwned { name } => format!("'{}' is already registered by someone else.", name),
            Self::AlreadyInProgress { name } => format!("A registration is already in progress for '{}'.", name),
            Self::NetworkRejected { reason } => format!("Registration was rejected: {}", reason),
            Self::Internal { .. } => "An internal error occurred during registration.".to_string(),
        }
    }

    /// Structured developer-facing details for [`ApiError`](crate::api_error::ApiError).
    pub fn details(&self) -> serde_json::Value {
        match self {
            Self::NetworkRejected { reason } => {
                serde_json::json!({ "reject_reason": reason.to_string() })
            }
            _ => serde_json::Value::Null,
        }
    }
}

impl PartialEq for ResolutionError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Offline, Self::Offline) => true,
            (
                Self::NotFound {
                    name: a_n,
                    peers_queried: a_p,
                },
                Self::NotFound {
                    name: b_n,
                    peers_queried: b_p,
                },
            ) => a_n == b_n && a_p == b_p,
            (
                Self::VdfVerificationFailed {
                    name: a_n,
                    count: a_c,
                },
                Self::VdfVerificationFailed {
                    name: b_n,
                    count: b_c,
                },
            ) => a_n == b_n && a_c == b_c,
            (
                Self::Expired {
                    name: a_n,
                    age: a_a,
                },
                Self::Expired {
                    name: b_n,
                    age: b_a,
                },
            ) => a_n == b_n && a_a == b_a,
            (
                Self::Timeout {
                    name: a_n,
                    elapsed_ms: a_e,
                    peers_queried: a_p,
                },
                Self::Timeout {
                    name: b_n,
                    elapsed_ms: b_e,
                    peers_queried: b_p,
                },
            ) => a_n == b_n && a_e == b_e && a_p == b_p,
            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
                a_m == b_m
            }
            _ => false,
        }
    }
}
impl Eq for ResolutionError {}

impl PartialEq for PublishError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Offline, Self::Offline) => true,
            (Self::InvalidProof(a), Self::InvalidProof(b)) => a == b,
            (Self::AlreadyOwned { name: a_n }, Self::AlreadyOwned { name: b_n }) => a_n == b_n,
            (Self::AllFailed { count: a_c }, Self::AllFailed { count: b_c }) => a_c == b_c,
            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
                a_m == b_m
            }
            _ => false,
        }
    }
}
impl Eq for PublishError {}

impl PartialEq for RegistrationError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::InvalidName { name: a_n }, Self::InvalidName { name: b_n }) => a_n == b_n,
            (Self::VdfFailed(a), Self::VdfFailed(b)) => a == b,
            (Self::CommitmentMismatch, Self::CommitmentMismatch) => true,
            (Self::AlreadyOwned { name: a_n }, Self::AlreadyOwned { name: b_n }) => a_n == b_n,
            (Self::AlreadyInProgress { name: a_n }, Self::AlreadyInProgress { name: b_n }) => {
                a_n == b_n
            }
            (Self::NetworkRejected { reason: a_r }, Self::NetworkRejected { reason: b_r }) => {
                a_r == b_r
            }
            (Self::Internal { message: a_m, .. }, Self::Internal { message: b_m, .. }) => {
                a_m == b_m
            }
            _ => false,
        }
    }
}
impl Eq for RegistrationError {}