Error Handling in Kinetic Network
Crate: kinetic-network Stage: 8 Reading time: 15 minutes Depends on: 01_overview.md
What Is This?
This document meticulously explains KineticStoreError, the fine-grained error enumeration used throughout the Kinetic network’s storage and event loop layers. It lives within kinetic-network/src/error.rs.
In a standard distributed hash table (DHT) like Kademlia, the reasons for rejecting a piece of incoming data are usually simple: the data is too large, or the node is out of storage space. But in Kinetic, rejecting a record is a complex, cryptoeconomic decision.
A record might be rejected because its Verifiable Delay Function (VDF) proof expired. It might be rejected because its Ed25519 signature is malformed. It might be rejected because it lost an XOR distance tie-breaker against an existing, equally valid record.
This file provides 21 specific, Kinetic-aware error variants. It allows the network layer to precisely identify why a domain name registration, heartbeat, or host routing update failed, rather than just throwing up its hands and knowing that “something went wrong.”
Without this file, debugging a live Kinetic node would be virtually impossible. If the network suddenly started rejecting thousands of records, we would have no programmatic way to determine if it was a coordinated attack, a bug in the time-sync code, or simply a surge in invalid domain registrations.
Why Kinetic Needs This
To understand why this file was created, you have to understand a fundamental limitation in libp2p, the underlying networking framework that Kinetic is built upon.
When you implement a custom record store in libp2p (which Kinetic does in order to validate records before storing them on disk), libp2p expects your validation function to return a specific error type: libp2p::kad::store::Error.
The problem is that libp2p’s built-in error enum is limited. For record rejections, it basically only offers a single variant: ValueTooLarge.
If Kinetic just returned ValueTooLarge every time a VDF failed, a signature was forged, or a heartbeat was stale, we would have no idea what was actually happening on the network. We wouldn’t know if we were under a cryptographic attack, if there was a clock sync issue causing stale records, or if the VDF engine had crashed. All we would see in the logs is endless “ValueTooLarge” messages.
KineticStoreError is Saif’s architectural solution to this bottleneck.
We define all 21 specific failure modes in this custom enum. When a record fails validation, we generate a KineticStoreError. We can then inspect it, log it with its exact context and severity, and map it to a stable KIN-NET-*** error code for external APIs.
Only after we have fully processed the Kinetic-specific error do we crush it down into libp2p’s generic ValueTooLarge error in order to satisfy the Rust compiler. This file forms the crucial bridge between Kinetic’s complex cryptoeconomic validation rules and libp2p’s simplistic, generic storage API.
How It Works
The entire file is built around a single Rust enum named KineticStoreError. It leverages popular Rust macros to automatically generate display text, and it includes several helper methods to categorize and process the errors.
Here is a step-by-step breakdown of how the error system functions:
1. Detailed Variant Breakdown
To fully grasp the scope of network validations, we need to look at exactly what each of these 21 errors represents. Kinetic is a hostile environment; peers will lie to you, send you garbage data, or attempt to overwrite your domain names. These variants are your node’s active defense mechanisms.
Payload and Capacity Constraints
PayloadTooLarge(KIN-NET-001): The record submitted by a peer exceeds the hard byte limit defined by the network. This prevents peers from spamming the DHT with multi-megabyte junk payloads.RateLimited(KIN-NET-017): The node is receiving too many reveal submissions too quickly. To protect local resources, it temporarily drops the record.NetworkHalted(KIN-NET-021): The ultimate defense mechanism. If the network is emergency-paused by the Root Key, all registrations and renewals are halted, and this error is returned.
Cryptographic VDF Failures Kinetic relies on Verifiable Delay Functions (VDFs) for domain ownership. When VDF proofs fail, these errors catch them:
VdfExpired(KIN-NET-002): VDFs have a strict shelf life. If a peer submits a proof that is too many rounds old, it is rejected. Note that this enum variant carries theagedata with it so we know exactly how stale the proof was.InvalidVdf(KIN-NET-003): The cryptographic math behind the VDF proof did not verify. The peer is either buggy or actively forging proofs.VdfEngineError(KIN-NET-004): The local VDF evaluation engine (thekyn-vdfintegration) crashed or returned an internal failure.InsufficientIterations(KIN-NET-009): A peer attempted to steal a domain name, but their VDF proof did not have more iterations than the existing record. The usurpation attempt is invalid.
Signature and Authentication Failures Every record in Kinetic must be cryptographically signed by the entity that generated it.
InvalidSignature(KIN-NET-005): The Ed25519 signature on the network payload does not match the data and public key provided.InvalidPublicKey(KIN-NET-006): The public key provided in the record is structurally malformed (e.g., wrong byte length).MalformedSignature(KIN-NET-007): The signature bytes themselves are structurally invalid.InvalidKidSignature(KIN-NET-011): Specifically for Kinetic Identity (KID) documents, the signature securing the document failed validation.InvalidManifestSignature(KIN-NET-012): The signature on a storage manifest failed.InvalidHostRouteSignature(KIN-NET-016): Finding 13 in the architecture—HostRoutingRecords must be signed. This error fires if that signature is bad or if the record’s timestamp is stale.
State and Logic Rejections Sometimes the cryptography is correct, but the DHT state rules reject the record.
TieBroken(KIN-NET-008): In Kademlia, if two peers generate a valid record for the exact same key at the exact same time, the network uses an XOR distance calculation against the peer’s Node ID to break the tie mathematically. This prevents infinite propagation loops. This error fires gracefully for the loser of that tie-break. It is an expected, normal network occurrence.RevealNotFound(KIN-NET-010): A peer attempted to perform an action on a domain, but the necessary reveal phase was never found in the DHT.StaleHeartbeat(KIN-NET-015): Finding 8 in the architecture—heartbeat updates must move forward in time. If a peer submits a heartbeat that is older than or equal to the current one, it is rejected to prevent replay attacks.StaleReveal(KIN-NET-018): The peer submitted a reveal, but the commitment is too recent (the required time delay period hasn’t elapsed).MissingCommitment(KIN-NET-019): The peer submitted a reveal without first registering a commitment hash in the DHT.
Formatting and Parsing Errors
UnknownRecordType(KIN-NET-013): The record payload prefix does not match any known Kinetic record type.InvalidDrandHex(KIN-NET-014): The randomness hex string pulled from the Drand beacon could not be decoded.InvalidName(KIN-NET-020): The requested apex domain name violates Kinetic’s stringent naming rules.
-> See: kinetic-network/src/error.rs — Lines 10 to 78
2. Structuring Errors with thiserror
Notice the #[error("...")] annotations directly above every variant in the source code.
Kinetic uses the thiserror crate to eliminate boilerplate. Instead of manually writing a Display implementation for KineticStoreError that uses a massive match statement to map every variant to a string, thiserror generates it automatically based on these annotations during compilation.
For variants with embedded data, like VdfExpired { age }, the macro effortlessly injects the age variable directly into the format string: #[error("VDF proof has expired ({age} rounds old)")]. This keeps the code clean while preserving rich context.
3. Stable Error Codes and RFC 7807
The code() method takes any KineticStoreError and returns a static string identifier like "KIN-NET-005". This is crucial for two distinct reasons: First, it enables powerful log parsing. When debugging the daemon, you can grep for "KIN-NET-008" to find all XOR tie-break losses without worrying about exact string matches in the human-readable log messages. Second, it provides API stability. When a user or a client application submits a record via the REST API, they need programmatic error codes to handle failures gracefully in their code. The human-readable text of an error might change in a future update, but KIN-NET-005 will always programmatically mean InvalidSignature.
Furthermore, the error_type_uri() method formats this stable error code into a URI: https://kinetic.network/errors/KIN-NET-001 This adheres to a web standard (RFC 7807) for returning errors in HTTP APIs. Even though this error originates deep in the Kademlia peer-to-peer layer, formatting it this way ensures that when it eventually bubbles up to the Stage 10 kinetic-rest API, it is already formatted for a web client to consume natively.
-> See: kinetic-network/src/error.rs — Lines 82 to 111
4. Human-Friendly Explanations
While code() is meant for machines, the user_message() method provides a clean, human-readable sentence for every single error. Instead of a client seeing KIN-NET-008 or an esoteric internal Rust enum name like TieBroken, user_message() maps this to "Record lost XOR tie-break against existing DHT entry". This separation of concerns ensures that the internal logs can be as technical as necessary, while the user-facing output remains understandable. The REST API leverages this method to populate the "message" field in its JSON error responses.
-> See: kinetic-network/src/error.rs — Lines 119 to 149
5. The Retryable Check
The is_retryable() method uses the Rust matches! macro to quickly determine if an error is transient.
For example, if you hit a RateLimited error or a VdfEngineError (perhaps the local VDF process briefly restarted), you can safely try the operation again. However, if you get an InvalidSignature error, retrying is pointless because the math will never verify no matter how many times you try. This helps the network loop decide whether to drop a peer or just back off temporarily.
-> See: kinetic-network/src/error.rs — Lines 114 to 116
6. Advanced Logging Based on Severity
Not all errors are equal. If a peer sends a malformed public key, that is a severe error indicating a bug or an attack. If a peer loses an XOR tie-break (TieBroken), that is normal network operation.
The severity() method maps every variant to a Severity level (Info, Warning, or Error). This feeds directly into the log_warning() method. This method does not just blindly print errors to standard out. It executes a match on the severity to route the log to the correct tracing macro level.
The tracing crate is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information. By feeding our errors into tracing::warn! and tracing::error! with attached key-value pairs (like error_code = error_code), we are not just printing strings; we are emitting structured JSON logs. This is how enterprise-grade monitoring systems track the health of the network in real-time.
-> See: kinetic-network/src/error.rs — Lines 152 to 202
7. The libp2p Escape Hatch
Finally, we reach the most important mechanical piece of the entire file:
#![allow(unused)]
fn main() {
impl From<KineticStoreError> for libp2p::kad::store::Error {
fn from(_e: KineticStoreError) -> Self {
libp2p::kad::store::Error::ValueTooLarge
}
}
}
This snippet uses Rust’s built-in From trait. Whenever a function deep inside libp2p expects a libp2p::kad::store::Error, but Kinetic’s custom validation logic produces our KineticStoreError, Rust automatically calls this from function to bridge the gap.
Before this conversion happens, Kinetic’s record store implementation intercepts the KineticStoreError, fires the rich log_warning() function to record the exact details (like "KIN-NET-015: StaleHeartbeat"), and then lets Rust convert it into a blind ValueTooLarge error to hand back to the libp2p framework. It is a necessary, deliberate hack to bypass libp2p’s rigid and uninformative error design.
-> See: kinetic-network/src/error.rs — Lines 206 to 210
Key Pieces
KineticStoreErrorEnum: The exhaustive list of 21 network rejection reasons. This enum derives thethiserror::Errortrait, which generates standard display formatting automatically based on macros. -> See file: kinetic-network/src/error.rs:L10-L78code()Function: Maps internal variants to staticKIN-NET-***string identifiers. This is essential for the REST API and automated network monitoring. -> See file: kinetic-network/src/error.rs:L81-L106error_type_uri()Function: Formats the error code into an RFC 7807 compliant URI for seamless HTTP API integration. -> See file: kinetic-network/src/error.rs:L109-L111user_message()Function: Provides a clean, client-facing string explaining the error without exposing internal enum names or raw system metrics. -> See file: kinetic-network/src/error.rs:L119-L149is_retryable()Function: A fast boolean check to see if an operation failed due to a temporary condition (like rate limiting) versus a permanent cryptographic failure. -> See file: kinetic-network/src/error.rs:L114-L116severity()Function: Maps every error into Info, Warning, or Error levels to dictate how the node should complain about the failure. -> See file: kinetic-network/src/error.rs:L152-L176From<KineticStoreError>Implementation: The trait implementation that quietly downgrades our rich 21-variant enum intolibp2p’s singleValueTooLargeerror so the underlying networking crate doesn’t panic. -> See file: kinetic-network/src/error.rs:L206-L210
How This Connects to the Rest of Kinetic
This file acts as the boundary layer between Kinetic’s strict validation rules and the standard networking stack.
- Upstream (Validation): When the Kademlia record store receives a
Putrequest from a peer, it passes the bytes to Kinetic’s validation logic. If validation fails for any reason, it generates one of these specificKineticStoreErrorvariants. - Downstream (libp2p): Because of the
Fromtrait implementation,libp2ponly ever seesValueTooLarge. This allows it to discard the invalid record without needing to understand Kinetic’s cryptoeconomic rules. - CROSS-CRATE:
Severity— This enum (Info, Warning, Error) is imported directly fromkinetic_core::error::Severity. It ensures that logging levels are consistent across all Kinetic crates. - FORWARD DEPENDENCY: The
KIN-NET-***codes defined incode()and formatted inerror_type_uri()will be utilized in Stage 10 (kinetic-rest). The REST API will intercept these errors and serve them as structured JSON to external clients so they know exactly why their transaction failed on the network.
Quick Reference
If you need to recall how network errors are categorized and processed:
- Stable Codes: The prefix is always
KIN-NET-. They range sequentially from001to021. - Retryable Errors: Only
RateLimitedandVdfEngineErrorare marked as retryable. All others are final rejections. - Severity Mappings:
- Info Level: Protocol collisions and timing bounds (TieBroken, InsufficientIterations, VdfExpired, RevealNotFound).
- Warning Level: Size or spam limit violations (PayloadTooLarge, RateLimited, UnknownRecordType).
- Error Level: All cryptographic verification failures, malformed data, and total network halts.
- libp2p Escape Mapping: Every single error in this file ultimately translates to
libp2p::kad::store::Error::ValueTooLargeat the absolute network boundary.
Open Questions / Things to Revisit
- The
ValueTooLargeHack: Right now, we log the rich error locally and then returnValueTooLargeto libp2p. Is there a scenario where we want libp2p to behave differently based on the exact error? For example, if a peer sends us anInvalidSignature, we might want libp2p to outright ban their IP at the swarm level, rather than just silently dropping the record as “too large.” We currently cannot instruct libp2p to penalize peers differently based on this error mapping. - Error Propagation to Local RPC: When a local RPC client submits a record to the daemon and it fails, does the local client successfully get the rich
KineticStoreError, or does it just get the crushedValueTooLargefrom the libp2p swarm? We need to ensure the local user gets the detailedKIN-NETcode on their command line, not the generic libp2p error. - Missing Infrastructure Categories: Should there be a specific error variant for “Disk Full” or “Database Locked” if the underlying record store backend fails to write, as opposed to a protocol validation failure? Currently, we only map logical rejections.