OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / dht_storage.h

358 lines · c ref:
#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