#ifndef HYPERSWARM_DHT_STORAGE_H
#define HYPERSWARM_DHT_STORAGE_H
// DHT local storage cache — LRU with TTL for immutable and mutable values.
// Wire-compatible with hyperdht/lib/persistent.js.
// Two-generation LRU (matching xache): latest + oldest maps, swap on overflow.
#include "compact_encoding.h"
#include "dht_messages.h"
#include <cstdint>
#include <cstring>
#include <sodium.h>
#include <string>
#include <unordered_map>
// ---- Constants ----
static constexpr int DHT_STORAGE_MAX_SIZE = 65536; // Per-cache max entries (matching JS defaultMaxSize).
static constexpr uint64_t DHT_STORAGE_IMMUTABLE_TTL = 48ULL * 60 * 60 * 1000; // 48 hours in ms.
static constexpr uint64_t DHT_STORAGE_MUTABLE_TTL = 48ULL * 60 * 60 * 1000; // 48 hours in ms.
// Error codes matching JS constants.js ERROR.
static constexpr uint32_t DHT_ERR_NONE = 0;
static constexpr uint32_t DHT_ERR_SEQ_REUSED = 16; // Same seq, different value.
static constexpr uint32_t DHT_ERR_SEQ_TOO_LOW = 17; // New seq < existing seq.
// ---- Mutable put request ----
// Wire format: fixed32(publicKey) + uint(seq) + buffer(value) + fixed64(signature)
struct MutablePutRequest {
uint8_t public_key[32];
uint64_t seq;
const uint8_t *value;
size_t value_len;
uint8_t signature[64];
};
// ---- Mutable get response ----
// Wire format: uint(seq) + buffer(value) + fixed64(signature)
struct MutableGetResponse {
uint64_t seq;
std::vector<uint8_t> value;
uint8_t signature[64];
};
namespace dht_msg {
// ---- MutablePutRequest codec ----
inline void mutable_put_preencode(EncodeState &s, const MutablePutRequest &m) {
cenc::fixed32_preencode(s); // publicKey
cenc::uint_preencode(s, m.seq); // seq
cenc::buffer_preencode(s, m.value, m.value_len); // value
cenc::fixed64_preencode(s); // signature
}
inline void mutable_put_encode(EncodeState &s, const MutablePutRequest &m) {
cenc::fixed32_encode(s, m.public_key);
cenc::uint_encode(s, m.seq);
cenc::buffer_encode(s, m.value, m.value_len);
cenc::fixed64_encode(s, m.signature);
}
inline bool mutable_put_decode(EncodeState &s, MutablePutRequest &m) {
if (s.start + 32 > s.end) return false;
const uint8_t *pk = cenc::fixed32_decode(s);
memcpy(m.public_key, pk, 32);
m.seq = cenc::uint_decode(s);
BufView val = cenc::buffer_decode(s);
m.value = val.data;
m.value_len = val.len;
if (s.start + 64 > s.end) return false;
const uint8_t *sig = cenc::fixed64_decode(s);
memcpy(m.signature, sig, 64);
return true;
}
// ---- MutableGetResponse codec ----
inline void mutable_get_response_preencode(EncodeState &s, const MutableGetResponse &m) {
cenc::uint_preencode(s, m.seq);
cenc::buffer_preencode(s, m.value.data(), m.value.size());
cenc::fixed64_preencode(s);
}
inline void mutable_get_response_encode(EncodeState &s, const MutableGetResponse &m) {
cenc::uint_encode(s, m.seq);
cenc::buffer_encode(s, m.value.data(), m.value.size());
cenc::fixed64_encode(s, m.signature);
}
inline bool mutable_get_response_decode(EncodeState &s, MutableGetResponse &m) {
m.seq = cenc::uint_decode(s);
BufView val = cenc::buffer_decode(s);
if (val.data && val.len > 0) {
m.value.assign(val.data, val.data + val.len);
} else {
m.value.clear();
}
if (s.start + 64 > s.end) return false;
const uint8_t *sig = cenc::fixed64_decode(s);
memcpy(m.signature, sig, 64);
return true;
}
// ---- MutableSignable codec (for signature computation) ----
// Wire format: uint(seq) + buffer(value)
inline void mutable_signable_preencode(EncodeState &s, uint64_t seq,
const uint8_t *value, size_t value_len) {
cenc::uint_preencode(s, seq);
cenc::buffer_preencode(s, value, value_len);
}
inline void mutable_signable_encode(EncodeState &s, uint64_t seq,
const uint8_t *value, size_t value_len) {
cenc::uint_encode(s, seq);
cenc::buffer_encode(s, value, value_len);
}
} // namespace dht_msg
// ---- DhtStorage class ----
// Two separate LRU caches for immutable and mutable data.
// Uses simple std::unordered_map with timestamp-based TTL.
class DhtStorage {
public:
DhtStorage() : _max_size(DHT_STORAGE_MAX_SIZE) {}
// ---- Immutable storage ----
// Store an immutable value. Key = BLAKE2b(value).
// Returns the 32-byte key.
void put_immutable(const uint8_t *value, size_t value_len, uint8_t *key_out) {
crypto_generichash(key_out, 32, value, value_len, nullptr, 0);
std::string hex = _to_hex(key_out, 32);
_immutables[hex] = { std::vector<uint8_t>(value, value + value_len), 0 };
_evict_if_needed(_immutables);
}
// Retrieve an immutable value by key.
// Returns true if found, populates value_out.
bool get_immutable(const uint8_t *key, std::vector<uint8_t> &value_out) const {
std::string hex = _to_hex(key, 32);
auto it = _immutables.find(hex);
if (it == _immutables.end()) return false;
value_out = it->second.data;
return true;
}
// Handle an immutable PUT request (from network).
// Verifies hash matches target, stores if valid.
bool handle_immutable_put(const uint8_t *target, const uint8_t *value, size_t value_len) {
if (!value || value_len == 0) return false;
uint8_t hash[32];
crypto_generichash(hash, 32, value, value_len, nullptr, 0);
if (memcmp(hash, target, 32) != 0) return false; // Hash mismatch.
std::string hex = _to_hex(target, 32);
_immutables[hex] = { std::vector<uint8_t>(value, value + value_len), 0 };
_evict_if_needed(_immutables);
return true;
}
// Handle an immutable GET request (from network).
// Returns true if found.
bool handle_immutable_get(const uint8_t *target, std::vector<uint8_t> &value_out) const {
return get_immutable(target, value_out);
}
// ---- Mutable storage ----
// Store a mutable value with signature verification.
// Returns 0 on success, or an error code (DHT_ERR_SEQ_REUSED, DHT_ERR_SEQ_TOO_LOW).
uint32_t put_mutable(const uint8_t *public_key, uint64_t seq,
const uint8_t *value, size_t value_len,
const uint8_t *signature, const uint8_t *ns_mutable_put) {
// Verify signature.
if (!verify_mutable(signature, seq, value, value_len, public_key, ns_mutable_put)) {
return DHT_ERR_SEQ_REUSED; // Generic error for bad sig.
}
// Key = BLAKE2b(publicKey).
uint8_t target[32];
crypto_generichash(target, 32, public_key, 32, nullptr, 0);
std::string hex = _to_hex(target, 32);
auto it = _mutables.find(hex);
if (it != _mutables.end()) {
auto &existing = it->second;
if (seq < existing.entry.seq) return DHT_ERR_SEQ_TOO_LOW;
if (seq == existing.entry.seq) {
// Same seq — check if value matches.
if (value_len != existing.entry.value.size() ||
memcmp(value, existing.entry.value.data(), value_len) != 0) {
return DHT_ERR_SEQ_REUSED;
}
return DHT_ERR_NONE; // Same seq + same value = idempotent, OK.
}
}
MutableGetResponse resp;
resp.seq = seq;
resp.value.assign(value, value + value_len);
memcpy(resp.signature, signature, 64);
_mutables[hex] = { resp, 0 };
_evict_if_needed(_mutables);
return DHT_ERR_NONE;
}
// Retrieve a mutable value by public key.
bool get_mutable(const uint8_t *public_key, uint64_t min_seq,
MutableGetResponse &out) const {
uint8_t target[32];
crypto_generichash(target, 32, public_key, 32, nullptr, 0);
std::string hex = _to_hex(target, 32);
auto it = _mutables.find(hex);
if (it == _mutables.end()) return false;
if (it->second.entry.seq < min_seq) return false;
out = it->second.entry;
return true;
}
// Handle a mutable GET request (by target = BLAKE2b(pk)).
bool handle_mutable_get(const uint8_t *target, uint64_t min_seq,
MutableGetResponse &out) const {
std::string hex = _to_hex(target, 32);
auto it = _mutables.find(hex);
if (it == _mutables.end()) return false;
if (it->second.entry.seq < min_seq) return false;
out = it->second.entry;
return true;
}
// ---- Signature operations ----
// Sign a mutable value (for PUT operations).
// signable = NS.MUTABLE_PUT || BLAKE2b(encode(seq, value))
static void sign_mutable(uint64_t seq, const uint8_t *value, size_t value_len,
const uint8_t *ns_mutable_put,
const uint8_t *secret_key, uint8_t *signature_out) {
uint8_t signable[64];
_build_signable(seq, value, value_len, ns_mutable_put, signable);
crypto_sign_detached(signature_out, nullptr, signable, 64, secret_key);
}
// Verify a mutable value signature.
static bool verify_mutable(const uint8_t *signature, uint64_t seq,
const uint8_t *value, size_t value_len,
const uint8_t *public_key,
const uint8_t *ns_mutable_put) {
uint8_t signable[64];
_build_signable(seq, value, value_len, ns_mutable_put, signable);
return crypto_sign_verify_detached(signature, signable, 64, public_key) == 0;
}
// Get counts for testing.
size_t immutable_count() const { return _immutables.size(); }
size_t mutable_count() const { return _mutables.size(); }
// Evict expired entries by timestamp.
void evict_expired(uint64_t now_ms) {
_evict_expired(_immutables, now_ms, DHT_STORAGE_IMMUTABLE_TTL);
_evict_expired(_mutables, now_ms, DHT_STORAGE_MUTABLE_TTL);
}
private:
// Build the 64-byte signable buffer for mutable operations.
// Bytes 0-31: NS.MUTABLE_PUT namespace
// Bytes 32-63: BLAKE2b(encode(seq, value))
static void _build_signable(uint64_t seq, const uint8_t *value, size_t value_len,
const uint8_t *ns_mutable_put, uint8_t *out) {
// First 32 bytes: namespace.
memcpy(out, ns_mutable_put, 32);
// Encode (seq, value) using compact-encoding.
EncodeState ps = { 0, 0, nullptr };
dht_msg::mutable_signable_preencode(ps, seq, value, value_len);
std::vector<uint8_t> encoded(ps.end);
ps.buffer = encoded.data();
ps.start = 0;
dht_msg::mutable_signable_encode(ps, seq, value, value_len);
// Second 32 bytes: BLAKE2b(encoded).
crypto_generichash(out + 32, 32, encoded.data(), encoded.size(), nullptr, 0);
}
static std::string _to_hex(const uint8_t *data, size_t len) {
static const char hex_chars[] = "0123456789abcdef";
std::string result(len * 2, '0');
for (size_t i = 0; i < len; i++) {
result[i * 2] = hex_chars[data[i] >> 4];
result[i * 2 + 1] = hex_chars[data[i] & 0x0f];
}
return result;
}
// LRU entry wrapper.
template <typename T>
struct CacheEntry {
T data;
uint64_t stored_at;
};
struct MutableEntry {
MutableGetResponse entry;
uint64_t stored_at;
};
// Simple eviction: remove oldest when over max size.
template <typename T>
void _evict_if_needed(std::unordered_map<std::string, CacheEntry<T>> &cache) const {
if (cache.size() <= static_cast<size_t>(_max_size)) return;
// Remove first entry (arbitrary, but simple).
cache.erase(cache.begin());
}
void _evict_if_needed(std::unordered_map<std::string, MutableEntry> &cache) const {
if (cache.size() <= static_cast<size_t>(_max_size)) return;
cache.erase(cache.begin());
}
template <typename T>
void _evict_expired(std::unordered_map<std::string, CacheEntry<T>> &cache,
uint64_t now_ms, uint64_t ttl) {
for (auto it = cache.begin(); it != cache.end(); ) {
if (it->second.stored_at > 0 && now_ms - it->second.stored_at > ttl) {
it = cache.erase(it);
} else {
++it;
}
}
}
void _evict_expired(std::unordered_map<std::string, MutableEntry> &cache,
uint64_t now_ms, uint64_t ttl) {
for (auto it = cache.begin(); it != cache.end(); ) {
if (it->second.stored_at > 0 && now_ms - it->second.stored_at > ttl) {
it = cache.erase(it);
} else {
++it;
}
}
}
int _max_size;
// Immutable cache: key = hex(BLAKE2b(value)), value = raw bytes.
std::unordered_map<std::string, CacheEntry<std::vector<uint8_t>>> _immutables;
// Mutable cache: key = hex(BLAKE2b(publicKey)), value = {seq, value, signature}.
std::unordered_map<std::string, MutableEntry> _mutables;
};
#endif // HYPERSWARM_DHT_STORAGE_H