Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


title: kinetic-core/src/types/dns.rs stage: 4 reading_time: 30 minutes depends_on: kinetic-types::dns, kinetic-core/src/error/dns.rs

What Is This?

This module serves as the boundary between the external internet and Kinetic’s internal DNS state machine.

It takes raw DNS zone data—submitted as JSON payloads—and enforces strict validation before allowing it to persist into the decentralized ledger.

In the Kinetic ecosystem, Domain Name System (DNS) resolution is not just about translating legacy domains like example.com into an IPv4 address. It is the fundamental routing layer for the decentralized web. Because of this dual mandate, this file handles both traditional internet DNS concepts (such as A, AAAA, CNAME, and TXT records) alongside Kinetic-native decentralized routing primitives (such as libp2p Peer IDs, Kinetic Key Identifiers, and IPFS content hashes).

Specifically, this file in kinetic-core implements the DnsZoneExt extension trait. This trait binds heavy, computationally expensive validation logic and memory-safe parsing directly to the bare, lightweight DnsZone data structures that are defined over in the kinetic-types crate.

Why Kinetic Needs This

When you build a decentralized peer-to-peer network, the data you receive from the outside world is fundamentally adversarial. You cannot trust the client submitting the data. If Kinetic validator nodes were to blindly accept whatever DNS JSON payloads were broadcasted to them, the network would collapse almost immediately due to a variety of attack vectors. Here is a detailed breakdown of why this specific validation layer is an absolute necessity:

  1. State Bloat Protection: In a decentralized network architecture, every full node has to store the global state of the network. Storage is a premium resource.

Warning

If a malicious actor could publish a single DNS zone containing 10,000 garbage records, they could rapidly fill up the hard drives of every node on the network, effectively executing a resource-exhaustion attack for pennies in transaction fees. This file steps in and enforces a strict, hard-coded 50-record cap per DNS zone to guarantee bounded state growth.

  1. Denial of Service (DoS) Prevention via JSON Bombs: Parsing JSON is computationally expensive and heavily utilizes the application’s call stack. Attackers often deploy “JSON bombs”—deeply nested recursive structures like [[[[{"a": "b"}]]]]. When standard JSON parsers attempt to evaluate these, they recurse so deeply that they blow past the operating system’s stack limit, instantly crashing the node. This module intentionally utilizes serde_json::from_slice which contains explicit, hard-coded recursion limits to protect the node’s memory architecture.
  2. Delayed Routing Integrity: If a user submits an invalid libp2p PeerId (e.g., a string missing a few characters) or a malformed IPFS CID, and the consensus network accepts it without checking, the network will not crash immediately. Instead, the error lies dormant in the database. Hours or days later, when an innocent client attempts to connect to that peer, the client’s routing engine will panic or fail. We must fail early and loudly. This file ensures that invalid routing parameters never make it into the state database.
  3. Standard DNS Specification Compatibility: Legacy DNS has strict, time- tested rules. For example, DNS RFCs mandate that a CNAME record acts as an exclusive alias—it cannot coexist on the same label as an IP address or a TXT record. Kinetic nodes must enforce these legacy rules flawlessly so that Kinetic domains can seamlessly bridge back to standard Web2 DNS resolvers (like Cloudflare or Google) without breaking them.
  4. Deterministic Cryptographic Verification: In a decentralized environment, nodes must prove ownership over their routing changes. The validation structures contained in this file ensure that the bytes being cryptographically signed by the user are highly deterministic and resistant to parsing ambiguities, preventing signature-spoofing attacks.

How It Works

This file orchestrates a highly robust, multi-stage processing pipeline. It takes an array of raw, untrusted bytes from the network and walks it through parsing, normalization, capacity checking, and deep cryptographic validation.

Stage 1: Safe and Bounded Payload Parsing

The entire process begins when the network receives a DNS update request. At this point, the request is nothing more than an array of raw bytes (&[u8]). -> See: kinetic-core/src/types/dns.rs — Lines 28-30

A naive implementation might convert these bytes into a Rust String (which requires the CPU to perform an expensive, full-pass UTF-8 validation) and then parse that string into JSON. Instead, the code directly invokes serde_json::from_slice. This function operates directly on the underlying byte array. Because it operates on bytes, it is extremely fast and can perform zero-copy deserialization where possible, meaning it borrows strings directly from the byte array instead of allocating new memory for them. More importantly, it utilizes Serde’s built-in recursion limits, protecting the node from the stack- overflow DoS attacks mentioned earlier. If the JSON is invalid, it throws a standard error, halting the transaction instantly.

Stage 2: Normalization and Case-Insensitivity

By standard definition, DNS labels are supposed to be entirely case-insensitive. A user visiting API.kinetic.host expects to be routed to the exact same destination as a user visiting api.kinetic.host. However, underneath the hood, the data is stored in a Rust HashMap. A hash map calculates hashes based on precise bytes, meaning it treats "API" and "api" as two completely distinct, non-colliding keys. -> See: kinetic-core/src/types/dns.rs — Lines 31-36

To solve this discrepancy without frustrating the user by rejecting their payload, the parser automatically normalizes the data. It achieves this by allocating a brand new HashMap. It then drains the contents of the original, raw parsed map. For every single key-value pair, it converts the key to lowercase. It utilizes the powerful Rust idiom: .entry(k.to_lowercase()).or_default().extend(v). This looks up the lowercase key in the new map. If it doesn’t exist, it gracefully inserts an empty vector. Then, it extends that vector with the incoming records. If a user poorly formatted their payload and submitted separate records for both API and api, this logic safely and efficiently merges all of those records into a single, unified api array.

Stage 3: Network Capacity Limits

Once the data structure is fully normalized, the validate() method takes command of the pipeline. -> See: kinetic-core/src/types/dns.rs — Lines 55-60

Before it even looks at the content of the records, it performs a purely quantitative network capacity check. It iterates over all the values (the vectors of records) contained within the hash map, maps them to their respective lengths, and sums them up into a single integer. If the total number of records across the entire DNS zone exceeds 50, the function immediately aborts, returning a TooManyRecords error.

Important

This is a critical security constraint. It definitively bounds the maximum size of a single DNS state entry in the blockchain, ensuring that validation times and state synchronization metrics remain predictable and lightweight.

Stage 4: Label Syntax Enforcement

Next, the validator enters a loop, evaluating every single label (the subdomain string, like www, api, or @) provided in the zone. -> See: kinetic-core/src/types/dns.rs — Lines 61-78

It applies a battery of string-manipulation checks to ensure compliance with DNS standards:

  • Length Limits: The label absolutely cannot be empty (length of 0), and it cannot exceed 63 characters in length. This is a strict limitation inherited from legacy DNS architecture.
  • Root and Wildcards: It explicitly bypasses further string-character checks if the label is precisely @ (which represents the root apex of the domain) or * (which represents a wildcard routing mechanism).
  • Hyphen Edge-Case Rules: The label cannot start with a hyphen, nor can it end with a hyphen (e.g., -www or api- are strictly invalid).
  • Character Allowlist: It iterates character by character over the string. If a character is not an ASCII alphanumeric, a hyphen, or an underscore, it throws an InvalidLabelCharacters error. No emojis or arbitrary Unicode are allowed in raw labels without punycode encoding.

Stage 5: CNAME Exclusivity and Collision Avoidance

-> See: kinetic-core/src/types/dns.rs — Lines 80-85

This stage enforces one of the most commonly misunderstood rules of DNS RFCs. If a label contains a CNAME (Canonical Name) record, that label is acting as a hard, redirecting alias to another domain entirely. Therefore, it is mathematically and logically impossible for that label to also contain an A record (an IP address) or a TXT record alongside it. The validator checks the array of records. If any single record is a CNAME variant, the code asserts that the total length of the array must be exactly 1. If the length is greater than 1, it realizes a collision has occurred and throws an InvalidCnameConfiguration error, saving the user from a broken domain setup.

Stage 6: Granular Record-Specific Validation

Finally, it matches on the specific enum variant of each individual record and applies bespoke, tight validation rules based on the intended destination. -> See: kinetic-core/src/types/dns.rs — Lines 87-122

  • TXT Records: Must be strictly bounded to 255 bytes or smaller, throwing TxtRecordTooLong if violated.
  • CNAME Targets: The destination domain string cannot be empty, and it cannot exceed 253 characters.
  • Peer IDs: The raw string is passed to libp2p_identity::PeerId::from_str. If the underlying libp2p cryptographic library cannot parse it into a valid, structurally sound peer identifier, the record is rejected. This guarantees routing integrity later in the node’s lifecycle.
  • KIDs (Kinetic Identifiers): Kinetic Identity Documents must strictly begin with the exact prefix did:kin:. If a user attempts to map a domain to a different decentralized identifier method (like did:eth: or did:pkh:), the system will emit a warning log and reject the payload, ensuring only native identities are bound.
  • IPFS CIDs: IPFS Content Identifiers must not be overly long (capped at 100 characters), and they must begin with either Qm (indicating an older CIDv0 Base58 encoded hash) or b (indicating a newer CIDv1 Base32 encoded hash).

Stage 7: Host Routing Serialization Protocol

While not part of the validate() function for the overarching zone, the HostRoutingRecord contains its own complex serialization pipeline for cryptographic validation. -> See: kinetic-types/src/dns.rs — Lines 61-79

When a routing record is signed, the signable_bytes() method constructs a deterministic byte array. It pre-allocates a vector with exact capacity to prevent memory reallocation lag. It then pushes the bytes in a highly structured format: first a network ID, then a hardcoded string -routing-v1, then the length of the host ID as a 4-byte big-endian integer, the host ID itself, the length of the peer ID as a 4-byte big-endian integer, the peer ID itself, and finally an 8-byte big-endian integer representing the Drand beacon timestamp. This rigidly structured byte payload guarantees that no two varying configurations can ever yield the same hash for signing.

Key Pieces

The Extension Trait Pattern (DnsZoneExt)

-> See: kinetic-core/src/types/dns.rs — Lines 9-16

-> See RUST_CONCEPTS.md for an explanation of the Extension Trait Pattern and the Orphan Rule. We use this to bolt validation logic onto DnsZone which is defined in the external kinetic-types crate.

DnsRecord Enum (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 19-40

This is a strongly typed enumeration defining every possible standard and decentralized DNS record type the Kinetic ecosystem understands. It utilizes the Serde attribute #[serde(tag = "type", content = "value")] to create an internally tagged representation for clean JSON parsing. Here is how the variants break down:

  • A(Ipv4Addr): Stores a standard IPv4 address object for legacy routing.
  • AAAA(Ipv6Addr): Stores a standard IPv6 address object.
  • CNAME(String): A canonical alias string pointing to another domain label entirely.
  • TXT(String): An arbitrary text field often utilized for domain ownership verification protocols.
  • PeerId(String): Contains a libp2p cryptographic peer identifier, bypassing IP routing entirely in favor of direct peer-to-peer transport.
  • KID(String): Contains a Kinetic Key Identifier (did:kin:...), mapping the domain to a globally authorized decentralized identity document.
  • IPFS(String): Contains a content identifier hash, allowing a domain to seamlessly serve decentralized static web pages stored on IPFS.
  • Other: The catch-all variant utilizing #[serde(other)]. If a newer network upgrade introduces a record type that an older node does not understand, this variant catches it, preventing a deserialization crash and ensuring forward- compatibility.

DnsZone Struct (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 11-17

What it does: A structural wrapper that encapsulates a HashMap<String, Vec<DnsRecord>>.

Why it matters: Notice the strategic #[serde(default)] attribute placed above the HashMap definition. If an end-user submits a JSON payload with an entirely empty body {} instead of the expected {"records": {}}, Serde will not panic and reject it. It will realize the records field is missing, look up the default implementation for a Rust HashMap (which is an empty map), and automatically substitute it. This makes the API endpoint extremely resilient and forgiving to slightly malformed but logically harmless inputs.

HostRoutingRecord (from kinetic-types)

-> See: kinetic-types/src/dns.rs — Lines 42-81

What it does: Maps a persistent, decentralized host identifier string to a fluid, currently active libp2p PeerId.

Why it matters: While standard DNS maps domains to static IP addresses, a peer-to-peer node’s active network connection (its peer ID and IP address) can fluctuate wildly as it disconnects, reboots, or changes networks. This record is highly dynamic. Crucially, look at the signable_bytes method provided in the implementation block. When a user applies a cryptographic signature to this record to prove they own the domain, the system does not just stringify the JSON and sign it. JSON keys can be arbitrarily rearranged by different libraries, which would cause the signature verification to fail unpredictably. Instead, the code aggressively packs the data into a strict, length-prefixed byte array format. By explicitly length-prefixing the individual strings before concatenating them together, it mathematically ensures that a clever attacker cannot dynamically shift bytes between different fields to trick the signature verification algorithm into validating a malicious payload. It also integrates a drand_kyn field. Instead of relying on a highly unreliable local UNIX timestamp to prove exactly when the routing record was generated, it utilizes the block number from the Drand distributed randomness beacon. This provides a globally verifiable, entirely decentralized clock mechanism, ensuring that attackers absolutely cannot replay older, intercepted routing records to maliciously hijack network traffic.

How This Connects to the Rest of Kinetic

CROSS-CRATE: The Divide Between kinetic-types and kinetic-core

A common question when viewing this file is: Why aren’t the DnsZone struct and the DnsZoneExt validation trait simply bundled together in the exact same file? The answer lies in strict dependency management. The kinetic-types crate is purposefully engineered to be universally importable across any platform architecture. A lightweight WebAssembly wallet running inside a mobile browser can import kinetic-types to properly serialize a DNS request. That browser environment does not need—and likely cannot support—the heavy cryptographic validation dependencies required to verify it, because the backend network will perform that validation anyway. By brutally splitting the raw data types from the complex validation logic, Kinetic maintains a very clean, modular, and fast- compiling dependency graph.

FORWARD DEPENDENCY: The State Transition Machine (Consensus)

When a validator node receives a transaction block containing a DNS state update payload, the core consensus engine will intercept the raw bytes and immediately pass them into the DnsZone::parse_payload(bytes) function defined in this file. If this function returns a validation error, the transaction is immediately marked as structurally invalid. The user loses their transaction gas fee, and the global state is never modified. The consensus layer trusts this specific file implicitly and exclusively to act as the ultimate gatekeeper of state purity.

FORWARD DEPENDENCY: The P2P Peer Routing Table

When a node resolves a domain like application.kin and receives a decentralized PeerId in response, it passes that raw string directly down into the libp2p networking stack to attempt a direct connection. If the string was malformed, the connection engine would panic or silently swallow the failure. Because this file strictly and definitively ran libp2p_identity::PeerId::from_str during the initial block validation phase, the routing layer can blindly trust that the string it pulled from the database is a cryptographically sound identifier, eliminating the need for redundant string checks during active routing.

Quick Reference

  • Maximum Total Records: Hard-capped at precisely 50 records per domain zone to prevent unmitigated state bloat.
  • Label Validation Parameters: Must be exactly 1 to 63 characters in length. Only alphanumeric characters, hyphens (but explicitly not at the leading or trailing edges), and underscores are permitted. The special @ (apex) and * (wildcard) labels bypass these specific string checks entirely.
  • Record Collision Mechanics: A label housing a CNAME redirect record cannot possess any other records whatsoever; it demands exclusivity.
  • Field Size Constraints: Standard TXT records are heavily constrained to 255 bytes. CNAME targets are constrained to 253 characters.
  • JSON Payload Structure: Enforces {"type": "...", "value": "..."} formatting via Serde internal tags, diverging from raw nested structs.
  • Data Normalization: All subdomain labels are automatically cast to lowercase before permanent storage. Duplicate labels submitted in varying cases (like Api and API) are seamlessly merged together utilizing hash map entry logic.
  • P2P Ecosystem Integrations: Strictly enforces the internal string structure of libp2p PeerIds and native Kinetic KIDs before they touch the database.
  • Dynamic Routing Safety: The HostRoutingRecord struct utilizes length- prefixed byte concatenation for deterministic cryptographic signing, anchored exclusively by a drand_kyn decentralized timestamp rather than local system clocks.

Open Questions / Things to Revisit

  1. TXT Record Constraints and Developer Experience: Should TXT records really be strictly limited to a single 255-byte string limitation? Legacy standard DNS actually allows for multiple 255-byte chunks that can be conceptually concatenated by the client application. If external application developers need to store large cryptographic verification keys (like a Matrix server verification token or a domain-ownership proof) directly inside a TXT record, this hard 255-byte limit could cause massive developer friction and require hacky protocol workarounds.
  2. The “Unknown Record” Silently Passing: The DnsRecord::Other fallback variant is an excellent architectural choice for forwards-compatibility, but during the validation loop, the code does exactly this: DnsRecord::Other => {}. It simply ignores it. This means a malicious actor could theoretically submit payloads filled entirely with Other records, bypassing all structural checks while still taking up valuable slots within the 50-record limit. Is this intentional behavior, or should Other records be explicitly rejected during active zone updates to prevent state-spamming?
  3. Rudimentary IPFS CID Validation Mechanics: The current CID validation logic simply checks if the provided string begins with Qm or b. This is extremely rudimentary and highly trusting of the user. It does not verify the Base58 or Base32 encoding, nor does it verify the cryptographic multihash structure nested inside the CID. This implies that a blatantly invalid CID (like the string b-this-is-garbage-data-that-will-fail) will easily pass the validation layer, which could cause downstream IPFS resolver clients to crash when they attempt to fetch the non-existent content. We should strongly consider importing the official cid crate to perform actual, rigorous parsing at the edge.