Kademlia Local Store (Part 1: Initialization and Pruning)
Crate: kinetic-network Stage: 8 Reading time: 30 minutes Depends on: docs/learn/core/01_overview.md, docs/learn/types/05_name_records.md, docs/learn/types/06_infrastructure.md
What Is This?
This module defines the KineticRecordStore, a customized, strict implementation of the standard libp2p RecordStore trait. In a typical, standard libp2p application (like a basic chat app or a simple file-sharing protocol), a RecordStore operates as a blind data bucket. It holds key-value pairs for the Distributed Hash Table (DHT) without asking any questions. When another node on the network connects to your node and asks you to store a piece of data (via a DHT put request), a standard RecordStore will blindly accept the payload bytes and save them directly into your local memory. It does not ask what the bytes mean. It does not verify if the bytes are valid or structurally sound. It simply acts as a passive storage layer for the routing protocol, assuming that all actors on the network are behaving honestly.
In the Kinetic architecture, operating under this assumption of honesty is catastrophic. Blindly trusting incoming DHT data is unacceptable and fundamentally insecure for a global namespace. The KineticRecordStore acts as a relentless cryptographic firewall placed directly between the chaotic, untrusted libp2p network and the node’s local persistent disk storage (managed by Sled). Instead of blindly accepting data, it actively intercepts all incoming records before they touch memory. It enforces strict domain validation rules, commit-reveal timelocks, VDF (Verifiable Delay Function) proof verification, and heartbeat liveness tracking before any byte is allowed to rest on the disk or in the active caches.
This specific document (Part 1) covers the first 290 lines of the core.rs file. These lines focus on the lifecycle management and state restoration of this store. It details how the KineticRecordStore boots up from a cold start, how it safely restores its memory from the persistent disk by re-verifying everything mathematically, and how it actively prunes stale data from the network as the global Drand clock progresses forward in time. The second half of the file (which handles the actual interception of put and get operations during runtime) will be covered in the next topic file.
Why Kinetic Needs This
To truly understand why this massive layer of complexity exists, you must compare the standard IPFS-style DHT model with the strict Kinetic model. Standard DHT implementations allow anyone, anywhere, to put any arbitrary data at any specific key. If Kinetic used the default MemoryStore provided out-of-the-box by libp2p, the network would instantly collapse under a deluge of malicious activity. An attacker with minimal resources could simply generate millions of fake domain names, attach random invalid cryptographic proofs, forge expired heartbeats, and push them to every node simultaneously. This attack vector would quickly overwrite valid domain records with garbage or exhaust the RAM of every honest node on the network, leading to a out-of-memory panic across the entire grid.
Kinetic requires a robust form of persistent, verified storage equipped with deterministic, self-executing eviction rules. When a node shuts down for maintenance and restarts, it should not have to query the global DHT all over again for domain names it had already verified prior to shutting down. VDF verification is CPU-intensive by design. If a node had to re-download and re-verify 10,000 domain names from the network every single time it booted up, node restarts would take hours, severely degrading the node operator’s experience. However, it also cannot blindly trust its own local disk upon waking up.
Why can’t it trust its own disk? Because the measurement of time in Kinetic is defined exclusively in Drand kyns. If a node is powered off for three weeks and then turns back on, the records stored safely on its local disk are now obsolete. The domain names have almost certainly passed their global resquaring epochs and now require new, updated VDF proofs. If the node simply loaded those old, unverified records from the Sled disk into its active memory and started serving them to the network, it would be caught distributing invalid, expired data. Honest peers would immediately flag this node as malicious or out-of-sync and ignore its routing responses entirely.
The Analogy: Imagine the node is a bouncer at an exclusive club. Standard libp2p says, “If anyone hands you an ID card, put it in the VIP box.” Kinetic libp2p says, “If anyone hands you an ID card, verify the cryptographic signature, ensure the expiration date hasn’t passed, and check the watermark. When the club closes for the night, lock the IDs in a safe. When the club opens the next morning, take the IDs out of the safe and verify them all over again, because some of them might have expired while you were sleeping.”
Therefore, the KineticRecordStore solves two critical and inherently opposing problems simultaneously:
- Survivability (Disk Backing): It backs up verified DHT records to an embedded, localized disk database (
Sled) so that they survive reboots. This saves the node from paying the exorbitant CPU cost of re-verifying the entire internet every time it starts. - Paranoia (Boot Re-verification): On boot, it assumes the disk itself is potentially stale and compromised by time. It re-verifies every single VDF proof and Ed25519 signature before loading it from disk into active memory. It discards anything that aged out while the node was offline.
- Decentralized Garbage Collection: Because blockless decentralized networks do not have a centralized server sending
DELETEbroadcasts, the store must continuously monitor the Drand kyn independently. It locally prunes records that have naturally expired according to the network’s universal mathematical rules.
Important
Without this exact, rigorous structure, Kinetic would have no long-term memory, no defense against time-based desynchronization attacks, and no capacity to maintain a clean, verifiable namespace.
How It Works
The first 290 lines of core.rs define the primary struct, its internal memory caches, and the critical new and prune lifecycle functions. Let’s break them down chronologically.
The Initialization Phase (new)
When the node boots, it immediately calls KineticRecordStore::new. The singular goal of this function is to build up the libp2p MemoryStore and our internal LRU caches, but it must do so without compromising the cryptographic integrity of the node.
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 65 to 160
}
-
Scanning the Disk for Names: The engine begins by scanning the local Sled database for any keys that start exactly with the
KRS_REVEAL_PREFIX. By using distinct prefixes, the database separates domain reveals from commitments and heartbeats. Crucially, the code limits this initial scan to100_000records. This is a hardcoded safety limit to prevent a massive disk (e.g., one that has collected millions of stale records over years) from exhausting the node’s RAM and CPU during a cold startup. -
Key Extraction and String Conversion: For every record found during the scan, the engine slices off the prefix bytes. It takes the remaining bytes and converts them into a
StringusingString::from_utf8_lossy. This gives the engine the actual domain name associated with the record. Usingfrom_utf8_lossyinstead of strict UTF-8 conversion ensures that if malformed data somehow ended up on disk (due to disk corruption or a weird edge case), the node will silently sanitize it rather than panicking and crashing during boot. -
Deserialization Attempt: It attempts to parse the raw JSON bytes attached to the key into a structured
NameRecord. If the JSON is corrupted, the record is simply ignored and skipped. -
The Re-Verification Gauntlet: Once deserialized, the record must prove itself valid under the current network conditions, from scratch. If the record is a
Premiumname (meaning it was injected directly by the decentralized governance system), it is considered implicitly valid. Premium names bypass the standard VDF checks because their authority comes from the blockchain layer consensus, not the individual VDF layer. If it is aStandardname, it must pass a strict cryptographic gauntlet:
- Iteration Check: The node computes the required VDF iterations based on the
initial_drand_kyn(which represents the exact current kyn the node woke up to). If the storediterationson the record are less than the calculated required amount, the record is obsolete and is immediately discarded. - Signature Check: Unless the node is configured to run in
dev_mode, it checks the Ed25519 signature of the record against the current network ID. This prevents records from testnets from bleeding into the mainnet database and causing namespace collisions. - Challenge Reconstruction: The node meticulously hashes the domain name, the cryptographic salt, the Drand randomness bytes (derived from the signed Drand pulse), and the owner’s public key.
This exact sequence reconstructs the 32-byte
Commitmentchallenge hash that the VDF was originally supposed to solve when the name was registered. - VDF Verification: Finally, it passes the reconstructed challenge, the proof bytes, and the claimed iterations into the
vdf_engine. Because VDF verification is CPU-intensive, the engine usestokio::task::block_in_placeon native architectures. This is a critical concurrency feature: it temporarily moves the current task to a blocking thread pool, ensuring that the heavy cryptographic math does not stall Tokio’s primary asynchronous executor thread, which is busy handling other network traffic.
- Caching the Survivors: Only the records that successfully survive this entire, brutal gauntlet are allowed to be loaded into the
reveals_by_namecache. Any record that fails is left behind on disk, isolated from memory, and will eventually be targeted for pruning.
The Architecture of Sled Storage Keys
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 78 to 83
}
When the node scans the Sled database, it relies on a very specific key architecture. Sled is a simple key-value store, which means it has no concept of tables or schemas. To emulate tables, Kinetic uses byte prefixes. The KRS_REVEAL_PREFIX is a hardcoded sequence of bytes that prepends the actual domain name. For example, if the domain name is saif.kyn, the key on disk is [KRS_REVEAL_PREFIX bytes] + [saif.kyn bytes]. When the scan occurs, the code checks if key_bytes.len() <= prefix_len. This is a boundary safety check. If the key exactly matches the prefix but has no domain name attached, it means a corrupted or empty record was somehow written to disk. The code continues, skipping the corrupted key to prevent out-of-bounds slicing panics when it tries to extract the domain name on the next line. This level of defensive programming is required when dealing with raw byte stores, as panicking during node initialization would cause the daemon loop to enter a crash loop.
The Cryptographic Hash Reconstruction
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 112 to 119
}
To verify a standard reveal, the node must rebuild the exact 32-byte hash that the VDF engine solved. It instantiates a clean Sha256 hasher and sequentially feeds it data. The order is deterministic:
hasher.update(reveal.name.as_bytes()): The raw UTF-8 bytes of the domain name.hasher.update(reveal.salt): A random 32-byte salt generated by the user to prevent rainbow table attacks on short domain names.hasher.update(&drand_rand): The 32-byte Drand randomness derived from hashing the Drand signature. This anchors the challenge to a specific point in time, preventing users from pre-computing VDFs years in advance.hasher.update(&reveal.pubkey): The 32-byte Ed25519 public key of the owner, binding the VDF proof exclusively to their identity so no one else can steal the proof mid-flight. After feeding all four components, the hasher finalizes into the 32-byteCommitment { hash }. If a single byte in any of those four components is altered, the resulting hash changes, and the VDF verification will decisively fail.
WebAssembly (WASM) Compatibility during Verification
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 121 to 134
}
Note
One of the most nuanced architectural details in the
newinitialization function is how it handles VDF verification across different target architectures. Kinetic is designed to run natively on servers, but it is also designed to compile to WebAssembly so that nodes can theoretically run inside a browser. When evaluating the VDF proof viavdf_engine.verify, the code uses conditional compilation flags (#[cfg]).
| Architecture | Details |
|---|---|
Native Architecture (not(target_arch = "wasm32")) | On native targets (like a Linux server or Mac), the node relies on Tokio for its asynchronous runtime. Because VDF verification is CPU-bound (it requires tight loops of sequential mathematical operations), running it directly on Tokio’s async thread would stall the executor. The node wouldn’t be able to respond to basic network pings while verifying the proof. To solve this, the code wraps the verification in tokio::task::block_in_place. This brilliantly moves the executing context to a dedicated blocking thread pool, freeing up Tokio to handle network I/O while the math computes in the background. |
WASM Architecture (target_arch = "wasm32") | WebAssembly in the browser does not natively support Tokio’s blocking thread pools because WASM typically runs in a single-threaded environment (or requires complex Web Worker setups). If the code attempted to use block_in_place in WASM, it would panic or fail to compile. Therefore, the code drops the block_in_place wrapper when compiling for WASM, running the verification synchronously. While this means a WASM node might temporarily freeze the UI thread while verifying a proof, it guarantees that the node can actually boot up and function in the browser. |
The Role of String::from_utf8_lossy
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 84
}
When scanning the Sled database, the system pulls raw bytes that represent the domain name. In a perfect world, these bytes are always flawlessly encoded UTF-8 strings. However, disks fail, bit flips occur, and malicious actors sometimes figure out ways to inject malformed byte sequences into the storage layer. If the engine used the standard String::from_utf8 method, a single corrupted record containing an invalid UTF-8 byte would throw an Err. If the node were using .unwrap() on that result, the entire node would panic and crash during the boot sequence, resulting in a bricked node that requires manual database deletion to fix. By using String::from_utf8_lossy, the node gracefully handles data corruption. If it encounters invalid bytes, it replaces them with the standard Unicode replacement character. While the resulting domain name will obviously fail subsequent cryptographic verification (because the hash of the sanitized string will not match the hash of the original challenge), the node itself will safely discard the record and continue booting. This is a critical defensive programming pattern for decentralized infrastructure.
The LRU Cache Mechanism
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 73 and 199
}
In decentralized networks, state bloat is a massive vector for attack. If an attacker can force your node to store infinite amounts of data in memory, they can crash your node via an Out-Of-Memory (OOM) panic. To prevent this, KineticRecordStore uses LruCache (Least Recently Used Cache) for reveals_by_name and accepted_reveals_timestamps. The capacity of this cache is defined by lru_cache_size, which is typed as a NonZeroUsize. This Rust type guarantees at compile-time that the capacity cannot accidentally be set to zero (which would cause the cache implementation to panic). When the network DHT grows to a size that exceeds this lru_cache_size, the node does not stop functioning. Instead, the LruCache smoothly ejects the oldest, least-queried records from active memory to make room for new ones. Because the records are still backed up in the Sled disk storage and accessible via the get_record_with_fallback method, the node can always retrieve them if someone queries them later. This limits the node’s maximum RAM utilization strictly, providing a deterministic upper bound on memory consumption regardless of how popular the Kinetic network becomes.
Hydrating the Heartbeats
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 162 to 175
}
After the domain names are verified, the node must restore its knowledge of infrastructure liveness across the network. It performs a second Sled scan, this time looking for keys starting specifically with the KRS_HB_PREFIX. For each heartbeat found, it extracts the domain name just like before. The value payload for a heartbeat is minimal: it is exactly 8 bytes long, representing a big-endian encoded u64 integer. This integer is the exact Drand kyn at which the heartbeat was originally recorded. The node converts these 8 bytes back into a u64 and inserts it into the last_heartbeats_by_name HashMap, ensuring the node remembers which infrastructure peers were alive immediately before the restart.
Hydrating the Kademlia Memory
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 177 to 191
}
Once the LRU cache is populated with verified records, the node must actually make them available to the standard libp2p Kademlia protocol. It creates a fresh, standard libp2p::kad::store::MemoryStore (referred to internally as inner). It then iterates over every single valid domain name that survived the boot gauntlet. For each name, it derives multiple deterministic Kademlia storage keys using the derive_storage_keys function. (This is necessary because a single domain name maps to multiple DHT coordinates to ensure high redundancy across the network). It wraps the JSON-serialized record into a kad::Record object and forcefully injects it into the inner memory store. Now, when a remote peer queries this node via the DHT, libp2p automatically serves the record directly from the inner store without requiring any extra custom routing logic from Kinetic.
The Pruning Phase (prune)
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 209 to 305
}
Because the DHT is a decentralized, headless entity, there is no central master server sending HTTP DELETE requests when data needs to be removed. Data removal must happen deterministically, locally, and simultaneously across all nodes, based purely on the passage of time (measured in Drand kyns). The prune function executes this critical garbage collection routine. It is called periodically during runtime. It checks three independent lifecycles to determine what should be deleted:
-
Pruning Commitments: It scans Sled for keys with the
KRS_COMMIT_PREFIX. It calculates the age usingcurrent_kyn.saturating_sub(kyn). If a commitment’s recorded kyn is older than 100 kyns relative to thecurrent_drand_kyn, it is targeted for deletion. The commit-reveal scheme requires the user to reveal their proof quickly to prevent front-running. A commitment that is 100 kyns old (roughly 5 minutes in a 3-second kyn network) is considered permanently abandoned. Deleting it prevents state bloat on the node’s disk. -
Pruning Resquaring Expirations: It scans the LRU cache for standard reveals. It calculates the age by subtracting the record’s
drand_kynfrom thecurrent_kyn. If this age exceeds the globalRESQUARING_EPOCH_KYNSconstant, the name registration has expired. The owner failed to resquare their VDF proof in time to maintain ownership, and therefore the network reclaims the name. The name is immediately queued for deletion. -
Pruning Heartbeat Timeouts: For domain names acting as infrastructure (like Relays or Validators), they must emit a continuous, periodic heartbeat to prove they are online. If the formula
current_kyn - last_heartbeatresults in a value that exceeds theidle_timeout(which translates to exactly 7 full days of Drand kyns), the infrastructure node is considered permanently dead. It is queued for deletion to ensure the network routing tables remain clean and accurate.
For any record queued for deletion, the store removes it from the memory caches (reveals_by_name and last_heartbeats_by_name). It then gathers all the associated Sled keys (including the derived Kademlia kad_record: libp2p prefixes) into a deletion queue array. Finally, it spawns a background blocking task to permanently erase these keys from the Sled disk. This multi-threaded deletion strategy ensures that writing to the slow mechanical or SSD disk does not hang the fast in-memory routing operations of the node.
Defensive Arithmetic with saturating_sub
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 218 and 233
}
In the prune function, the code constantly calculates the age of records by subtracting the record’s kyn from the current_drand_kyn. It uses current_kyn.saturating_sub(kyn). In Rust, standard subtraction (-) can panic in debug mode or wrap around in release mode if the right side is larger than the left side. Why would a record’s kyn ever be larger than the current kyn? If a node’s local Drand daemon temporarily desynchronizes, or if the node receives a malicious record that claims to be from the future, standard subtraction would instantly crash the node with an integer underflow panic. saturating_sub neutralizes this attack vector. If kyn is greater than current_kyn, saturating_sub simply returns 0. This logically means the record is 0 kyns old (from the node’s perspective), which prevents it from being prematurely pruned, but more importantly, prevents the node from crashing.
Key Pieces
KineticRecordStore
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 25 to 43
}
This struct is the main architectural bridge between libp2p’s standard Kademlia routing and Kinetic’s proprietary verified storage logic. Let’s break down every field:
-
inner: kad::store::MemoryStoreThis is the standard libp2p memory store. We let libp2p handle the low-level DHT query responses out of this store, but we act as the bouncer, controlling what is allowed to be placed into it. Everything in here has passed verification. -
storage: Arc<dyn StorageEngine>A thread-safe, atomically reference-counted pointer to the persistent disk database (Sled). This provides the vital persistence layer across node reboots. By using a dynamic trait object (dyn StorageEngine), the network remains agnostic to the actual storage implementation, allowing for easy mocking during unit tests. -
vdf_engine: Arc<dyn kinetic_core::traits::VdfEngine>The mathematical backend engine used to evaluate and re-verify VDF proofs during startup and runtime. It performs the heavy lifting for the verification gauntlet. -
reveals_by_name: LruCache<String, NameRecord>An active in-memory cache of verified domain names. We specifically use an LRU (Least Recently Used) cache to guarantee that a node will never run out of RAM, even if the total size of the global DHT grows to millions of records. When the cache is full, the oldest un-queried record is silently dropped from memory, protecting the node from crashing. -
last_heartbeats_by_name: HashMap<String, u64>A mapping that tracks the highest Drand kyn at which a specific infrastructure node successfully pulsed. It is used exclusively during theprunecycle to detect and evict dead relays that have stopped responding. -
accepted_reveals_timestamps: LruCache<String, VecDeque<web_time::Instant>>A secondary LRU cache that tracks the local system time of when reveals were accepted for specific names. This is used exclusively for local rate-limiting to prevent a malicious peer from spamming millions of micro-updates to a single name and causing unnecessary, thrashing disk writes that could burn out an SSD. This usesweb_timerather thanstd::timeto remain compatible with WASM environments where standard system time might be mocked or restricted. -
current_drand_kyn: u64The node’s internal state tracker for the current progression of time on the network. It dictates what is valid and what is expired. -
max_reveals_per_hour: usizeA configuration variable dictating how many updates a single domain name is allowed to push to the network within a strict one-hour window.
The Role of local_peer_id
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 54
}
When initializing the store, the node must pass its own local_peer_id into the libp2p::kad::store::MemoryStore::new(local_peer_id) constructor. In Kademlia, the DHT is a massive XOR metric space. Every node and every piece of data has an address in this space. By providing the local_peer_id, the internal memory store knows exactly where the node resides in the network topology. This allows the store to determine which records it is “closest” to, which dictates which records it should cache and which records it should drop when memory gets tight.
Governance Overrides (Premium Names)
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Lines 143 to 147
}
During the validation gauntlet, there is a distinct branch for kinetic_core::types::NameRecord::Premium { .. }. Premium names are exempt from the grueling VDF verification process. Why? Because premium domains (like core.kyn or relay.kyn) are not claimed via computational work. They are injected directly into the network state via the decentralized governance system. Because their legitimacy is already guaranteed by the blockchain consensus layer, forcing the node to verify a VDF proof for them would be redundant and wasteful. The system instantly flags them as is_valid = true; and pushes them into the LRU cache.
Handling Network ID in Signatures
#![allow(unused)]
fn main() {
// -> See: kinetic-network/src/store/core.rs — Line 100
}
When the signature of a domain name is verified during the startup gauntlet, the verify_signature method expects a specific parameter: kinetic_core::constants::NETWORK_ID. In decentralized systems, cross-network replay attacks are a constant threat. An attacker could theoretically take a valid, proven domain name registration from the Kinetic testnet and maliciously broadcast it on the Kinetic mainnet. Because the VDF proof itself is computationally sound regardless of which network it resides on, a naive system would accept the testnet record and overwrite the mainnet namespace, causing chaos and namespace collisions. To prevent this, the network ID is cryptographically bound directly into the Ed25519 signature payload by the client. When the node verifies the signature on boot, it asserts that the signature is exclusively valid for its currently configured network ID. If a testnet record accidentally ends up on a mainnet disk (perhaps via a manual database copy error by the node operator), it will instantly fail this signature check and be discarded safely without crashing the node.
How This Connects to the Rest of Kinetic
- CROSS-CRATE:
StorageEngine— Defined in thekinetic-corecrate. This is the abstract trait that our Sled database implementation fulfills. - CROSS-CRATE:
NameRecord— Defined and explained indocs/learn/types/05_name_records.md. The fundamental data structure representing a registered domain name on the network. - CROSS-CRATE:
VdfEngine— Defined in thekinetic-corecrate. Evaluates the mathematical validity of a given claim. - CROSS-CRATE:
derive_storage_keys— Defined inkinetic-core/src/types/keys.rs. The deterministic function that calculates the exact DHT network coordinates for a given domain name.
Quick Reference
- Initialization Filter: The
KineticRecordStore::newfunction scans the Sled disk, re-verifies VDF proofs computationally, and populates the in-memory DHT. - Pruning Commitments: Commitments are permanently deleted from disk if they are older than 100 kyns (roughly 5 minutes).
- Pruning Domain Names: Domain names are permanently deleted from the network if their age exceeds the
RESQUARING_EPOCH_KYNSlimit. - Pruning Heartbeats: Infrastructure names are permanently deleted if no heartbeat has been heard for 7 days worth of kyns.
- LRU Cache Protection: The store deliberately uses LRU caches to prevent node RAM exhaustion, providing a hard cap on memory usage regardless of the global DHT size.
- Saturating Subtraction: Prevents node crashes when calculating age if time becomes temporarily desynchronized.
Open Questions / Things to Revisit
Warning
- Startup Blocking Time: The
newinitialization function executes atokio::task::block_in_placeduring a massiveforloop that iterates over up to100_000records from the Sled database. If the node has a populated database of near exactly 100,000 records, evaluating 100,000 sequential VDF proofs synchronously could take an exorbitant amount of real-world time. This could delay node startup significantly, leading to network desynchronization immediately upon boot. Should this verification process be batched, parallelized using Rayon, or executed fully asynchronously instead of blocking?- Pruning Race Conditions: In the
prune()method, a background blocking task is spawned to delete keys from the slow Sled disk. If a valid network update for that exact domain name arrives from the DHT immediately after the pruning task starts, is it possible that the background task accidentally deletes the newly written valid record, causing a temporary data eclipse on the local node?