OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_phase7.cpp

664 lines · cpp ref:
// 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;
}