Crate: kinetic-kid
Stage: 1 - Core Identity and Parsing
Reading Time: 15 minutes
Depends On: error.rs (for KidError)
What Is This?
This file defines KineticDid, a custom Rust type that wraps a standard String to represent a Decentralized Identifier (DID) specifically for the Kinetic network.
It ensures that any string claiming to be a Kinetic DID strictly adheres to the format did:kin:<64-character-lowercase-hex>.
Because of the way this struct is built, it is literally impossible to have a KineticDid in your program that contains a malformed identifier.
If the incoming data is invalid, it is caught and rejected at the system boundary before the struct is even allowed to exist.
This strictness gives us immense confidence when passing it around the Kinetic codebase.
The identifier consists of two main parts:
- The scheme prefix, which is always expected to be
did:kin:. - The method-specific identifier, which is a 64-character, all-lowercase hexadecimal string. This 64-character string is typically the hex-encoded SHA-256 hash of a public key or another underlying identity primitive in the Kinetic system.
Why Kinetic Needs This
You might wonder: why go through the trouble of creating a whole struct just to hold a single string?
Why not just pass a normal String around in our functions?
If a Kinetic networking function takes a String as an argument representing a DID, it has no idea if that string is a valid DID, an empty string, or complete gibberish sent by a malicious peer.
The function would either have to blindly trust the caller or re-validate the string itself, over and over again in every part of the codebase.
If a developer forgets to validate it just once, invalid data poisons the system and could corrupt the Kinetic network state.
This helps us avoid entire classes of bugs (like network spoofing) just by using a strong type.
Note
See
RUST_CONCEPTS.mdfor an explanation of the “Newtype” pattern and the “Parse, don’t validate” philosophy used here. By wrapping a standard type in a custom struct (KineticDid), we force all parsing and validation to happen at the system boundary before the struct is constructed.
How It Works
The core logic of did.rs happens at the boundaries where a String tries to become a KineticDid.
#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 6 to 9
}
Notice the #[derive(Debug, Clone, PartialEq, Eq, Hash)] macro above the struct.
Note
See
RUST_CONCEPTS.mdfor#[derive(...)]. This ensures our custom struct can be copied, compared for equality, and used as a key in HashMaps (e.g., mapping a DID to a peer connection).
#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 20 to 45
}
The new function is the gatekeeper. It performs four distinct, sequential checks, returning a specific variant of KidError the moment one fails:
- First, it pulls in the required prefix using the
env!("KINETIC_DID_PREFIX")macro. This macro actually reads an environment variable at compile time and bakes it into the binary. It verifies the string starts with this prefix. - It uses string slicing (
&id_str[expected_prefix.len()..]) to safely extract the method-specific ID part of the string, and checks if it’s empty. String slices are fast, zero-copy views into the original string. - It explicitly checks that the method-specific ID is exactly 64 characters long, ensuring it matches the expected SHA-256 hex length.
- Finally, it validates that every single character in the ID is correct.
Let’s look at the character validation closely:
#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 35 to 40
}
The code method_specific_id.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()) performs the check efficiently.
Note
See
RUST_CONCEPTS.mdfor Closures & Iterators (used here via.chars().all(...)to validate each character with a short-circuiting check).
#![allow(unused)]
fn main() {
// See: kinetic-kid/src/did.rs — Lines 69 to 78
}
Another crucial part of how this works is the custom Deserialize implementation.
By default, if you use a #[derive(Deserialize)] macro on a Newtype, Rust would blindly put the incoming network string directly into the private id field, bypassing our validation entirely.
We override this behavior by implementing the Deserialize trait manually.
When JSON or binary data arrives over the Kinetic network, our custom Deserialize block intercepts it.
First, it uses the standard deserializer to parse the raw bytes into a normal, unverified String.
Then, it immediately passes that unverified String to our strict KineticDid::new() function.
If new() succeeds, we get our safe KineticDid.
If new() fails, the entire network payload parsing is rejected and an error is thrown (serde::de::Error::custom).
Important
This guarantees that invalid DIDs cannot even enter the running application memory space. This makes debugging network payloads incredibly reliable.
Key Pieces
| Component | Description |
|---|---|
KineticDid struct (Lines 6-9) | The actual Newtype struct. It contains a single private field: id: String. The privacy here is the most critical part—because id is private, no other code anywhere in Kinetic can manually construct this struct or modify the string inside it, preserving the validity guarantee. |
| Derived Traits (Line 6) | Clone for copying, PartialEq/Eq for checking equality, and Hash so DIDs can be used as keys in network routing tables or peer lists. |
KineticDid::new (Lines 20-45) | The constructor and sole validation boundary. It returns a Result<Self, KidError>, forcing the caller to proactively handle the possibility that the input string is invalid. |
as_str method (Lines 48-50) | A safe way to read the underlying valid string as a reference without consuming or destroying the struct. |
Display implementation (Lines 53-57) | This wires up the struct to Rust’s standard formatting macros. It allows a KineticDid to be easily printed using println!("{}", did) or converted back into a string, outputting the raw internal string transparently without wrapping it in quotes or struct syntax. |
Serialize and Deserialize (Lines 60-78) | The active shields at the network boundaries, ensuring no serialized payload goes out malformed, and no deserialized payload comes in unverified. |
proptests module (Lines 80-91) | Uses property-based testing (proptest!) to generate random garbage data (like the regex pattern "\\PC*") and feeds it into the parser to guarantee the code will never panic or crash on unexpected input. |
How This Connects to the Rest of Kinetic
FORWARD DEPENDENCY: Any component in the Kinetic network dealing with user identities, peer nodes, or cryptographic signatures will rely heavily on KineticDid.
For example, when building the P2P networking layer or the underlying database schema, functions and structs will accept a KineticDid type instead of a String.
This completely removes the need to clutter their business logic with string format validation, making the system much safer and easier to read.
CROSS-CRATE: Network payloads defined in other crates (like peer discovery messages or consensus blocks) will use KineticDid as a field type.
Because of our custom Deserialize logic, the network parsing layer will automatically enforce our DID rules before the application logic even sees the incoming message.
If a peer sends a malicious DID, the network layer drops it before the core node logic is even aware of it.
By doing this, we guarantee that all network communication relies entirely on strictly verified DIDs.
Quick Reference
| Action / Concept | Details |
|---|---|
| To create a DID | let my_did = KineticDid::new("did:kin:a1b2c3d4...")?; (Note the ? to handle the KidError if it fails). |
| To safely get the string back out | let raw_str = my_did.as_str(); |
| To print it to the console | println!("Connected to peer: {}", my_did); |
| Serialization boundary | KineticDid guarantees it will reject invalid peer data eagerly at the network edge during JSON or binary parsing. |
| Underlying structure | A private string wrapper leveraging the Newtype pattern for compiler-enforced safety. |
Open Questions / Things to Revisit
- Compile-time environment variable: Using
env!("KINETIC_DID_PREFIX")means thedid:kin:prefix is baked into the binary at compile time. Is there any scenario where we’d want this to be configurable at runtime, or isdid:kin:totally permanent for the lifespan of the project? - Proptests Expansion: The file currently includes a basic property-based test (Lines 80-91) ensuring the parser doesn’t panic or crash on garbage input strings. We should likely expand these proptests to ensure specific edge-case boundary conditions (like exactly 63 characters vs 65 characters) explicitly return the correct
KidErrorvariants, rather than just checking for crashes. - Memory Allocation: Currently,
KineticDidwraps a heap-allocatedString. If we pass these around millions of times per second during consensus, the memory allocations could add up. We might want to revisit this later and explore using a fixed-size byte array (like[u8; 32]) internally to avoid heap allocations entirely while retaining the exact same Newtype safety guarantees.