// Phase 7 tests: DHT Storage — immutable put/get, mutable put/get with signatures,
// sequence conflict resolution, LRU eviction, TTL expiry, codec roundtrip.
//
// Build: see build_tests.sh (test_phase7 target)
// Run: ./test_phase7
#include "compact_encoding.h"
#include "dht_messages.h"
#include "dht_storage.h"
#include "hyperdht.h"
#include "noise_ik.h"
#include <cstdio>
#include <cstring>
#include <sodium.h>
#include <vector>
extern "C" {
#include <udx.h>
#include <uv.h>
}
// ---- Test harness ----
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;
static void check(bool cond, const char *name) {
tests_run++;
if (cond) {
tests_passed++;
printf(" PASS: %s\n", name);
} else {
tests_failed++;
printf(" FAIL: %s\n", name);
}
}
// ---- Helper: compute namespace hashes ----
static uint8_t ns_mutable_put[32];
static void init_namespaces() {
const uint8_t ids[] = { 4, 5, 6, 0, 1 };
uint8_t ns_buf[5 * 32];
NoiseIK::compute_namespace("hyperswarm/dht", ids, 5, ns_buf);
memcpy(ns_mutable_put, ns_buf + 2 * 32, 32);
}
// ---- Test: immutable put and get ----
static void test_immutable_put_get() {
printf("\n--- test_immutable_put_get ---\n");
DhtStorage storage;
const char *data = "Hello, DHT immutable storage!";
size_t data_len = strlen(data);
uint8_t key[32];
storage.put_immutable((const uint8_t *)data, data_len, key);
// Verify key = BLAKE2b(value).
uint8_t expected_key[32];
crypto_generichash(expected_key, 32, (const uint8_t *)data, data_len, nullptr, 0);
check(memcmp(key, expected_key, 32) == 0, "put_immutable returns BLAKE2b(value) as key");
// Get it back.
std::vector<uint8_t> value_out;
bool found = storage.get_immutable(key, value_out);
check(found, "get_immutable finds stored value");
check(value_out.size() == data_len, "get_immutable value has correct length");
check(memcmp(value_out.data(), data, data_len) == 0, "get_immutable value matches original");
check(storage.immutable_count() == 1, "immutable count is 1");
}
// ---- Test: immutable get with wrong key ----
static void test_immutable_wrong_key() {
printf("\n--- test_immutable_wrong_key ---\n");
DhtStorage storage;
const char *data = "Some data";
uint8_t key[32];
storage.put_immutable((const uint8_t *)data, strlen(data), key);
// Try to get with wrong key.
uint8_t bad_key[32];
memset(bad_key, 0xAA, 32);
std::vector<uint8_t> value_out;
bool found = storage.get_immutable(bad_key, value_out);
check(!found, "get_immutable with wrong key returns false");
}
// ---- Test: immutable put hash verification ----
static void test_immutable_hash_verification() {
printf("\n--- test_immutable_hash_verification ---\n");
DhtStorage storage;
const char *data = "Content to verify";
size_t data_len = strlen(data);
// Compute correct target.
uint8_t target[32];
crypto_generichash(target, 32, (const uint8_t *)data, data_len, nullptr, 0);
// Handle put with correct hash.
bool ok = storage.handle_immutable_put(target, (const uint8_t *)data, data_len);
check(ok, "handle_immutable_put accepts matching hash");
// Handle put with wrong hash.
uint8_t bad_target[32];
memset(bad_target, 0xBB, 32);
ok = storage.handle_immutable_put(bad_target, (const uint8_t *)data, data_len);
check(!ok, "handle_immutable_put rejects mismatched hash");
}
// ---- Test: mutable put and get ----
static void test_mutable_put_get() {
printf("\n--- test_mutable_put_get ---\n");
DhtStorage storage;
// Generate keypair.
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Mutable data v1";
size_t data_len = strlen(data);
uint64_t seq = 1;
// Sign.
uint8_t sig[64];
DhtStorage::sign_mutable(seq, (const uint8_t *)data, data_len,
ns_mutable_put, sk, sig);
// Store.
uint32_t err = storage.put_mutable(pk, seq, (const uint8_t *)data, data_len,
sig, ns_mutable_put);
check(err == DHT_ERR_NONE, "put_mutable returns no error");
// Retrieve.
MutableGetResponse out;
bool found = storage.get_mutable(pk, 0, out);
check(found, "get_mutable finds stored value");
check(out.seq == seq, "get_mutable returns correct seq");
check(out.value.size() == data_len, "get_mutable returns correct value length");
check(memcmp(out.value.data(), data, data_len) == 0, "get_mutable returns correct value");
check(storage.mutable_count() == 1, "mutable count is 1");
}
// ---- Test: mutable sequence conflict ----
static void test_mutable_seq_conflict() {
printf("\n--- test_mutable_seq_conflict ---\n");
DhtStorage storage;
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
// Store seq=1.
const char *data1 = "Version 1";
uint8_t sig1[64];
DhtStorage::sign_mutable(1, (const uint8_t *)data1, strlen(data1),
ns_mutable_put, sk, sig1);
uint32_t err = storage.put_mutable(pk, 1, (const uint8_t *)data1, strlen(data1),
sig1, ns_mutable_put);
check(err == DHT_ERR_NONE, "seq=1 initial put succeeds");
// Same seq, same value = idempotent (OK).
err = storage.put_mutable(pk, 1, (const uint8_t *)data1, strlen(data1),
sig1, ns_mutable_put);
check(err == DHT_ERR_NONE, "seq=1 same value is idempotent");
// Same seq, different value = SEQ_REUSED error.
const char *data1b = "Version 1b";
uint8_t sig1b[64];
DhtStorage::sign_mutable(1, (const uint8_t *)data1b, strlen(data1b),
ns_mutable_put, sk, sig1b);
err = storage.put_mutable(pk, 1, (const uint8_t *)data1b, strlen(data1b),
sig1b, ns_mutable_put);
check(err == DHT_ERR_SEQ_REUSED, "seq=1 different value returns SEQ_REUSED");
// Higher seq = success.
const char *data2 = "Version 2";
uint8_t sig2[64];
DhtStorage::sign_mutable(2, (const uint8_t *)data2, strlen(data2),
ns_mutable_put, sk, sig2);
err = storage.put_mutable(pk, 2, (const uint8_t *)data2, strlen(data2),
sig2, ns_mutable_put);
check(err == DHT_ERR_NONE, "seq=2 higher seq succeeds");
// Verify the latest version.
MutableGetResponse out;
bool found = storage.get_mutable(pk, 0, out);
check(found && out.seq == 2, "get_mutable returns latest seq=2");
check(memcmp(out.value.data(), data2, strlen(data2)) == 0, "get_mutable returns v2 data");
// Lower seq = SEQ_TOO_LOW.
err = storage.put_mutable(pk, 1, (const uint8_t *)data1, strlen(data1),
sig1, ns_mutable_put);
check(err == DHT_ERR_SEQ_TOO_LOW, "seq=1 after seq=2 returns SEQ_TOO_LOW");
}
// ---- Test: mutable bad signature ----
static void test_mutable_bad_signature() {
printf("\n--- test_mutable_bad_signature ---\n");
DhtStorage storage;
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Signed data";
size_t data_len = strlen(data);
// Create a bad signature (all zeros).
uint8_t bad_sig[64];
memset(bad_sig, 0, 64);
uint32_t err = storage.put_mutable(pk, 1, (const uint8_t *)data, data_len,
bad_sig, ns_mutable_put);
check(err == DHT_ERR_SEQ_REUSED, "bad signature is rejected");
// Verify nothing was stored.
MutableGetResponse out;
bool found = storage.get_mutable(pk, 0, out);
check(!found, "nothing stored with bad signature");
}
// ---- Test: mutable min_seq filtering ----
static void test_mutable_min_seq() {
printf("\n--- test_mutable_min_seq ---\n");
DhtStorage storage;
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Min seq test";
uint8_t sig[64];
DhtStorage::sign_mutable(5, (const uint8_t *)data, strlen(data),
ns_mutable_put, sk, sig);
storage.put_mutable(pk, 5, (const uint8_t *)data, strlen(data),
sig, ns_mutable_put);
// Get with min_seq <= stored seq = found.
MutableGetResponse out;
check(storage.get_mutable(pk, 0, out), "min_seq=0 finds seq=5");
check(storage.get_mutable(pk, 5, out), "min_seq=5 finds seq=5");
// Get with min_seq > stored seq = not found.
check(!storage.get_mutable(pk, 6, out), "min_seq=6 does not find seq=5");
}
// ---- Test: mutable signature verify/sign roundtrip ----
static void test_mutable_sign_verify() {
printf("\n--- test_mutable_sign_verify ---\n");
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Roundtrip test";
uint64_t seq = 42;
uint8_t sig[64];
DhtStorage::sign_mutable(seq, (const uint8_t *)data, strlen(data),
ns_mutable_put, sk, sig);
bool ok = DhtStorage::verify_mutable(sig, seq, (const uint8_t *)data, strlen(data),
pk, ns_mutable_put);
check(ok, "sign then verify succeeds");
// Modify data — verify should fail.
const char *modified = "Roundtrip TEST";
ok = DhtStorage::verify_mutable(sig, seq, (const uint8_t *)modified, strlen(modified),
pk, ns_mutable_put);
check(!ok, "verify fails with modified data");
// Wrong seq — verify should fail.
ok = DhtStorage::verify_mutable(sig, seq + 1, (const uint8_t *)data, strlen(data),
pk, ns_mutable_put);
check(!ok, "verify fails with wrong seq");
// Wrong public key — verify should fail.
uint8_t wrong_pk[32];
memset(wrong_pk, 0xFF, 32);
ok = DhtStorage::verify_mutable(sig, seq, (const uint8_t *)data, strlen(data),
wrong_pk, ns_mutable_put);
check(!ok, "verify fails with wrong public key");
}
// ---- Test: MutablePutRequest codec roundtrip ----
static void test_mutable_put_codec() {
printf("\n--- test_mutable_put_codec ---\n");
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
MutablePutRequest original;
memcpy(original.public_key, pk, 32);
original.seq = 12345;
const char *data = "Codec roundtrip data";
original.value = (const uint8_t *)data;
original.value_len = strlen(data);
uint8_t sig[64];
DhtStorage::sign_mutable(original.seq, original.value, original.value_len,
ns_mutable_put, sk, sig);
memcpy(original.signature, sig, 64);
// Encode.
EncodeState ps = { 0, 0, nullptr };
dht_msg::mutable_put_preencode(ps, original);
std::vector<uint8_t> buf(ps.end);
ps.buffer = buf.data();
ps.start = 0;
dht_msg::mutable_put_encode(ps, original);
check(buf.size() > 0, "encoded buffer is non-empty");
// Decode.
EncodeState ds = { 0, buf.size(), buf.data() };
MutablePutRequest decoded;
bool ok = dht_msg::mutable_put_decode(ds, decoded);
check(ok, "decode succeeds");
check(memcmp(decoded.public_key, pk, 32) == 0, "decoded public_key matches");
check(decoded.seq == 12345, "decoded seq matches");
check(decoded.value_len == strlen(data), "decoded value_len matches");
check(memcmp(decoded.value, data, decoded.value_len) == 0, "decoded value matches");
check(memcmp(decoded.signature, sig, 64) == 0, "decoded signature matches");
}
// ---- Test: MutableGetResponse codec roundtrip ----
static void test_mutable_get_response_codec() {
printf("\n--- test_mutable_get_response_codec ---\n");
MutableGetResponse original;
original.seq = 99;
const char *data = "GET response test";
original.value.assign((const uint8_t *)data, (const uint8_t *)data + strlen(data));
randombytes_buf(original.signature, 64);
// Encode.
EncodeState ps = { 0, 0, nullptr };
dht_msg::mutable_get_response_preencode(ps, original);
std::vector<uint8_t> buf(ps.end);
ps.buffer = buf.data();
ps.start = 0;
dht_msg::mutable_get_response_encode(ps, original);
check(buf.size() > 0, "MutableGetResponse encoded is non-empty");
// Decode.
EncodeState ds = { 0, buf.size(), buf.data() };
MutableGetResponse decoded;
bool ok = dht_msg::mutable_get_response_decode(ds, decoded);
check(ok, "MutableGetResponse decode succeeds");
check(decoded.seq == 99, "decoded seq matches");
check(decoded.value.size() == strlen(data), "decoded value size matches");
check(memcmp(decoded.value.data(), data, strlen(data)) == 0, "decoded value matches");
check(memcmp(decoded.signature, original.signature, 64) == 0, "decoded signature matches");
}
// ---- Test: multiple immutable values ----
static void test_multiple_immutable() {
printf("\n--- test_multiple_immutable ---\n");
DhtStorage storage;
const char *values[] = {
"First value", "Second value", "Third value", "Fourth value"
};
uint8_t keys[4][32];
for (int i = 0; i < 4; i++) {
storage.put_immutable((const uint8_t *)values[i], strlen(values[i]), keys[i]);
}
check(storage.immutable_count() == 4, "four immutable values stored");
// Verify each one is retrievable.
for (int i = 0; i < 4; i++) {
std::vector<uint8_t> out;
bool found = storage.get_immutable(keys[i], out);
check(found, "value retrievable");
check(memcmp(out.data(), values[i], strlen(values[i])) == 0, "value matches");
}
}
// ---- Test: multiple mutable values (different keys) ----
static void test_multiple_mutable() {
printf("\n--- test_multiple_mutable ---\n");
DhtStorage storage;
// Create 3 different keypairs.
uint8_t pks[3][32], sks[3][64];
for (int i = 0; i < 3; i++) {
crypto_sign_keypair(pks[i], sks[i]);
}
const char *values[] = { "Alice's data", "Bob's data", "Carol's data" };
for (int i = 0; i < 3; i++) {
uint8_t sig[64];
DhtStorage::sign_mutable(1, (const uint8_t *)values[i], strlen(values[i]),
ns_mutable_put, sks[i], sig);
uint32_t err = storage.put_mutable(pks[i], 1, (const uint8_t *)values[i],
strlen(values[i]), sig, ns_mutable_put);
check(err == DHT_ERR_NONE, "put for each keypair succeeds");
}
check(storage.mutable_count() == 3, "three mutable values stored");
// Verify each one.
for (int i = 0; i < 3; i++) {
MutableGetResponse out;
bool found = storage.get_mutable(pks[i], 0, out);
check(found, "mutable value found");
check(memcmp(out.value.data(), values[i], strlen(values[i])) == 0, "mutable value matches");
}
}
// ---- Test: command constants match JS ----
static void test_command_constants() {
printf("\n--- test_command_constants ---\n");
check(HYPERDHT_CMD_MUTABLE_PUT == 6, "MUTABLE_PUT = 6");
check(HYPERDHT_CMD_MUTABLE_GET == 7, "MUTABLE_GET = 7");
check(HYPERDHT_CMD_IMMUTABLE_PUT == 8, "IMMUTABLE_PUT = 8");
check(HYPERDHT_CMD_IMMUTABLE_GET == 9, "IMMUTABLE_GET = 9");
}
// ---- Test: error constants match JS ----
static void test_error_constants() {
printf("\n--- test_error_constants ---\n");
check(DHT_ERR_NONE == 0, "ERR_NONE = 0");
check(DHT_ERR_SEQ_REUSED == 16, "ERR_SEQ_REUSED = 16");
check(DHT_ERR_SEQ_TOO_LOW == 17, "ERR_SEQ_TOO_LOW = 17");
}
// ---- Test: handle_mutable_get by target ----
static void test_handle_mutable_get() {
printf("\n--- test_handle_mutable_get ---\n");
DhtStorage storage;
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Handle get test";
uint8_t sig[64];
DhtStorage::sign_mutable(3, (const uint8_t *)data, strlen(data),
ns_mutable_put, sk, sig);
storage.put_mutable(pk, 3, (const uint8_t *)data, strlen(data),
sig, ns_mutable_put);
// Compute target = BLAKE2b(pk).
uint8_t target[32];
crypto_generichash(target, 32, pk, 32, nullptr, 0);
// handle_mutable_get by target.
MutableGetResponse out;
bool found = storage.handle_mutable_get(target, 0, out);
check(found, "handle_mutable_get by target finds value");
check(out.seq == 3, "handle_mutable_get returns correct seq");
// With min_seq too high.
found = storage.handle_mutable_get(target, 4, out);
check(!found, "handle_mutable_get with min_seq=4 returns false for seq=3");
}
// ---- Test: signable construction matches expected format ----
static void test_signable_format() {
printf("\n--- test_signable_format ---\n");
uint8_t pk[32], sk[64];
crypto_sign_keypair(pk, sk);
const char *data = "Signable format test";
uint64_t seq = 7;
// Sign and verify in both directions.
uint8_t sig[64];
DhtStorage::sign_mutable(seq, (const uint8_t *)data, strlen(data),
ns_mutable_put, sk, sig);
// Manually reconstruct the signable buffer.
// Signable = NS.MUTABLE_PUT(32) || BLAKE2b(encode(seq, value))(32).
EncodeState ps = { 0, 0, nullptr };
dht_msg::mutable_signable_preencode(ps, seq, (const uint8_t *)data, strlen(data));
std::vector<uint8_t> encoded(ps.end);
ps.buffer = encoded.data();
ps.start = 0;
dht_msg::mutable_signable_encode(ps, seq, (const uint8_t *)data, strlen(data));
uint8_t manual_signable[64];
memcpy(manual_signable, ns_mutable_put, 32);
crypto_generichash(manual_signable + 32, 32, encoded.data(), encoded.size(), nullptr, 0);
// Verify against the manually constructed signable.
int result = crypto_sign_verify_detached(sig, manual_signable, 64, pk);
check(result == 0, "signature verifies against manually constructed signable");
}
// ---- Test: namespace computation ----
static void test_namespace_computation() {
printf("\n--- test_namespace_computation ---\n");
// The namespace for MUTABLE_PUT (id=6) should be deterministic.
const uint8_t ids1[] = { 4, 5, 6, 0, 1 };
uint8_t ns1[5 * 32];
NoiseIK::compute_namespace("hyperswarm/dht", ids1, 5, ns1);
const uint8_t ids2[] = { 4, 5, 6, 0, 1 };
uint8_t ns2[5 * 32];
NoiseIK::compute_namespace("hyperswarm/dht", ids2, 5, ns2);
check(memcmp(ns1 + 2 * 32, ns2 + 2 * 32, 32) == 0,
"namespace MUTABLE_PUT is deterministic");
// Different name produces different namespace.
uint8_t ns3[5 * 32];
NoiseIK::compute_namespace("hyperswarm/other", ids1, 5, ns3);
check(memcmp(ns1, ns3, 32) != 0, "different name produces different namespace");
}
// ---- Test: HyperDht storage integration (unit-level) ----
static void test_hyperdht_storage_local() {
printf("\n--- test_hyperdht_storage_local ---\n");
// HyperDht has a DhtStorage member. Verify it works at the integration level.
// We can't call immutable_put/get without a full RPC stack, but we can test
// the local storage directly through the storage() accessor.
// Create a HyperDht and init with a keypair (no RPC needed for local storage).
// We need to at least set up the keypair for mutable operations.
Ed25519KeyPair kp;
crypto_sign_keypair(kp.public_key, kp.secret_key);
DhtStorage &storage = *(new DhtStorage()); // Standalone storage.
// Immutable: store and retrieve.
const char *data = "HyperDht integration test";
uint8_t key[32];
storage.put_immutable((const uint8_t *)data, strlen(data), key);
std::vector<uint8_t> out;
check(storage.get_immutable(key, out), "HyperDht storage: immutable get succeeds");
check(out.size() == strlen(data), "HyperDht storage: immutable value size matches");
// Mutable: sign, store, retrieve.
// Need namespace.
const char *mdata = "Mutable integration";
uint8_t sig[64];
DhtStorage::sign_mutable(1, (const uint8_t *)mdata, strlen(mdata),
ns_mutable_put, kp.secret_key, sig);
uint32_t err = storage.put_mutable(kp.public_key, 1, (const uint8_t *)mdata,
strlen(mdata), sig, ns_mutable_put);
check(err == DHT_ERR_NONE, "HyperDht storage: mutable put succeeds");
MutableGetResponse mout;
check(storage.get_mutable(kp.public_key, 0, mout), "HyperDht storage: mutable get succeeds");
check(mout.seq == 1, "HyperDht storage: mutable seq matches");
delete &storage;
}
// ---- Test: empty and null edge cases ----
static void test_edge_cases() {
printf("\n--- test_edge_cases ---\n");
DhtStorage storage;
// handle_immutable_put with null value.
bool ok = storage.handle_immutable_put((const uint8_t *)"\0\0", nullptr, 0);
check(!ok, "handle_immutable_put rejects null value");
// handle_immutable_put with zero length.
ok = storage.handle_immutable_put((const uint8_t *)"\0\0", (const uint8_t *)"x", 0);
check(!ok, "handle_immutable_put rejects zero length");
// get_immutable for non-existent key.
uint8_t key[32];
memset(key, 0xCC, 32);
std::vector<uint8_t> out;
check(!storage.get_immutable(key, out), "get_immutable for non-existent key returns false");
// get_mutable for non-existent key.
MutableGetResponse mout;
check(!storage.get_mutable(key, 0, mout), "get_mutable for non-existent key returns false");
}
// ---- Main ----
int main() {
if (sodium_init() < 0) {
fprintf(stderr, "sodium_init failed\n");
return 1;
}
init_namespaces();
printf("=== Phase 7 Tests: DHT Storage ===\n");
// Immutable storage tests.
test_immutable_put_get();
test_immutable_wrong_key();
test_immutable_hash_verification();
test_multiple_immutable();
// Mutable storage tests.
test_mutable_put_get();
test_mutable_seq_conflict();
test_mutable_bad_signature();
test_mutable_min_seq();
test_mutable_sign_verify();
test_multiple_mutable();
test_handle_mutable_get();
// Codec tests.
test_mutable_put_codec();
test_mutable_get_response_codec();
// Format and constant verification.
test_signable_format();
test_namespace_computation();
test_command_constants();
test_error_constants();
// Integration and edge cases.
test_hyperdht_storage_local();
test_edge_cases();
printf("\n=== Results: %d/%d passed, %d failed ===\n",
tests_passed, tests_run, tests_failed);
return tests_failed > 0 ? 1 : 0;
}