OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_phase3.cpp

700 lines · cpp ref:
// Phase 3 tests: HyperDHT message codecs, namespaces, Noise IK, SecurePayload.
// Build: g++ -std=c++17 -I../src test_phase3.cpp ../src/noise_ik.cpp -lsodium -o test_phase3
// Run: ./test_phase3

#include "compact_encoding.h"
#include "dht_messages.h"
#include "noise_ik.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>

static int tests_run = 0;
static int tests_passed = 0;

static void hex_to_bytes(const char *hex, uint8_t *out, size_t *out_len) {
	size_t len = strlen(hex) / 2;
	for (size_t i = 0; i < len; i++) {
		unsigned int byte;
		sscanf(hex + i * 2, "%02x", &byte);
		out[i] = static_cast<uint8_t>(byte);
	}
	*out_len = len;
}

static void bytes_to_hex(const uint8_t *data, size_t len, char *out) {
	for (size_t i = 0; i < len; i++) {
		sprintf(out + i * 2, "%02x", data[i]);
	}
	out[len * 2] = '\0';
}

#define EXPECT_HEX(name, data, len, expected_hex) do { \
	tests_run++; \
	char hex_buf[4096]; \
	bytes_to_hex(data, len, hex_buf); \
	if (strcmp(hex_buf, expected_hex) == 0) { \
		tests_passed++; \
		printf("  PASS: %s\n", name); \
	} else { \
		printf("  FAIL: %s\n    expected: %s\n    got:      %s\n", name, expected_hex, hex_buf); \
	} \
} while(0)

#define EXPECT_EQ(name, actual, expected) do { \
	tests_run++; \
	if ((actual) == (expected)) { \
		tests_passed++; \
		printf("  PASS: %s\n", name); \
	} else { \
		printf("  FAIL: %s (expected %lld, got %lld)\n", name, \
			   (long long)(expected), (long long)(actual)); \
	} \
} while(0)

#define EXPECT_TRUE(name, cond) do { \
	tests_run++; \
	if (cond) { \
		tests_passed++; \
		printf("  PASS: %s\n", name); \
	} else { \
		printf("  FAIL: %s\n", name); \
	} \
} while(0)

// ---- Test namespace computation ----
static void test_namespaces() {
	printf("\n=== Namespace computation ===\n");

	// Compute namespaces using NoiseIK::compute_namespace
	// IDs: [4(ANNOUNCE), 5(UNANNOUNCE), 6(MUTABLE_PUT), 0(PEER_HANDSHAKE), 1(PEER_HOLEPUNCH)]
	uint8_t ids[] = { 4, 5, 6, 0, 1 };
	uint8_t ns_out[5 * 32];
	NoiseIK::compute_namespace("hyperswarm/dht", ids, 5, ns_out);

	// From JS test vectors:
	EXPECT_HEX("NS_ANNOUNCE", ns_out + 0*32, 32,
		"36386adddf9f6fd60db83a6f42fc159d1146aa8644037664230aaa1f0179d497");
	EXPECT_HEX("NS_UNANNOUNCE", ns_out + 1*32, 32,
		"ded293cd93fb395e756ecf5fff426529e72c36eacc22e5ed944d9099a2561e32");
	EXPECT_HEX("NS_MUTABLE_PUT", ns_out + 2*32, 32,
		"668e823edd5ce7f5338d68bf1161f4a3c28ce437ee2ab49efd30d99366039b1e");
	EXPECT_HEX("NS_PEER_HANDSHAKE", ns_out + 3*32, 32,
		"14d6d4b49214ab1033ed204976caa258bae9e1e8543b9ad1fd996a910b0c4e3a");
	EXPECT_HEX("NS_PEER_HOLEPUNCH", ns_out + 4*32, 32,
		"f1191cd5e67b10b54a507033280ed1ff0e12278268d5679c8f93d417210d168b");
}

// ---- Test NoisePayload encode/decode ----
static void test_noise_payload() {
	printf("\n=== NoisePayload encoding ===\n");

	// Test 1: basic (4 bytes) - version=1, flags=0, error=0, firewall=0
	{
		dht_msg::NoisePayload p;
		memset(&p, 0, sizeof(p));
		p.version = 1;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::noise_payload_preencode(s, p);
		EXPECT_EQ("basic preencode size", s.end, 4);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::noise_payload_encode(s, p);
		EXPECT_HEX("basic encode", buf, s.start, "01000000");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::NoisePayload dp;
		dht_msg::noise_payload_decode(ds, dp);
		EXPECT_EQ("basic decode version", dp.version, 1u);
		EXPECT_EQ("basic decode error", dp.error, 0u);
		EXPECT_EQ("basic decode firewall", dp.firewall, 0u);
		EXPECT_EQ("basic decode consumed all", ds.start, ds.end);
	}

	// Test 2: with addresses4 and firewall=CONSISTENT
	{
		dht_msg::NoisePayload p;
		memset(&p, 0, sizeof(p));
		p.version = 1;
		p.firewall = 2;
		p.addresses4[0] = { {127, 0, 0, 1}, 10240 };
		p.addresses4[1] = { {192, 168, 1, 1}, 4242 };
		p.addresses4_count = 2;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::noise_payload_preencode(s, p);
		EXPECT_EQ("addresses4 preencode size", s.end, 17);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::noise_payload_encode(s, p);
		EXPECT_HEX("addresses4 encode", buf, s.start,
			"01020002027f0000010028c0a801019210");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::NoisePayload dp;
		dht_msg::noise_payload_decode(ds, dp);
		EXPECT_EQ("addresses4 decode firewall", dp.firewall, 2u);
		EXPECT_EQ("addresses4 decode count", dp.addresses4_count, 2);
		EXPECT_EQ("addresses4[0] port", dp.addresses4[0].port, 10240u);
		EXPECT_EQ("addresses4[1] port", dp.addresses4[1].port, 4242u);
		EXPECT_EQ("addresses4 decode consumed all", ds.start, ds.end);
	}

	// Test 3: with addresses4 + UDX info
	{
		dht_msg::NoisePayload p;
		memset(&p, 0, sizeof(p));
		p.version = 1;
		p.firewall = 1;
		p.addresses4[0] = { {10, 0, 0, 1}, 12345 };
		p.addresses4_count = 1;
		p.has_udx = true;
		p.udx.reusable_socket = false;
		p.udx.id = 7;
		p.udx.seq = 100;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::noise_payload_preencode(s, p);
		EXPECT_EQ("udx preencode size", s.end, 15);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::noise_payload_encode(s, p);
		EXPECT_HEX("udx encode", buf, s.start,
			"010a0001010a000001393001000764");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::NoisePayload dp;
		dht_msg::noise_payload_decode(ds, dp);
		EXPECT_EQ("udx decode firewall", dp.firewall, 1u);
		EXPECT_TRUE("udx decode has_udx", dp.has_udx);
		EXPECT_EQ("udx decode id", dp.udx.id, 7u);
		EXPECT_EQ("udx decode seq", dp.udx.seq, 100u);
		EXPECT_EQ("udx decode consumed all", ds.start, ds.end);
	}

	// Test 4: with UDX + secretStream
	{
		dht_msg::NoisePayload p;
		memset(&p, 0, sizeof(p));
		p.version = 1;
		p.firewall = 1;
		p.has_udx = true;
		p.udx.reusable_socket = true;
		p.udx.id = 1;
		p.udx.seq = 0;
		p.has_secret_stream = true;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::noise_payload_preencode(s, p);
		EXPECT_EQ("secretStream preencode size", s.end, 9);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::noise_payload_encode(s, p);
		EXPECT_HEX("secretStream encode", buf, s.start,
			"011800010101010001");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::NoisePayload dp;
		dht_msg::noise_payload_decode(ds, dp);
		EXPECT_TRUE("secretStream decode has_udx", dp.has_udx);
		EXPECT_TRUE("secretStream decode has_secret_stream", dp.has_secret_stream);
		EXPECT_TRUE("secretStream decode reusable", dp.udx.reusable_socket);
		EXPECT_EQ("secretStream decode consumed all", ds.start, ds.end);
	}
}

// ---- Test HandshakeMessage encode/decode ----
static void test_handshake_message() {
	printf("\n=== HandshakeMessage encoding ===\n");

	// Test 1: mode=1 with relay address
	{
		uint8_t noise[32];
		memset(noise, 0xAA, 32);

		HandshakeMessage m;
		memset(&m, 0, sizeof(m));
		m.mode = 1;
		m.noise = noise;
		m.noise_len = 32;
		m.has_peer_address = false;
		m.has_relay_address = true;
		m.relay_address = { {88, 99, 3, 86}, 49737 };

		EncodeState s = { 0, 0, nullptr };
		dht_msg::handshake_preencode(s, m);
		EXPECT_EQ("relay preencode size", s.end, 41);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::handshake_encode(s, m);
		EXPECT_HEX("relay encode", buf, s.start,
			"020120aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa5863035649c2");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		HandshakeMessage dm;
		dht_msg::handshake_decode(ds, dm);
		EXPECT_EQ("relay decode mode", dm.mode, 1u);
		EXPECT_EQ("relay decode noise_len", dm.noise_len, 32u);
		EXPECT_TRUE("relay decode has_relay", dm.has_relay_address);
		EXPECT_TRUE("relay decode no peer", !dm.has_peer_address);
		EXPECT_EQ("relay decode relay port", dm.relay_address.port, 49737u);
		EXPECT_EQ("relay decode consumed all", ds.start, ds.end);
	}

	// Test 2: mode=0 with peer + relay addresses
	{
		uint8_t noise[81];
		memset(noise, 0xBB, 81);

		HandshakeMessage m;
		memset(&m, 0, sizeof(m));
		m.mode = 0;
		m.noise = noise;
		m.noise_len = 81;
		m.has_peer_address = true;
		m.peer_address = { {10, 0, 0, 1}, 12345 };
		m.has_relay_address = true;
		m.relay_address = { {88, 99, 3, 86}, 49737 };

		EncodeState s = { 0, 0, nullptr };
		dht_msg::handshake_preencode(s, m);
		EXPECT_EQ("both addrs preencode size", s.end, 96);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::handshake_encode(s, m);
		EXPECT_HEX("both addrs encode", buf, s.start,
			"030051bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0a00000139305863035649c2");

		EXPECT_EQ("both addrs consumed all", s.start, s.end);
	}
}

// ---- Test HolepunchPayload encode/decode ----
static void test_holepunch_payload() {
	printf("\n=== HolepunchPayload encoding ===\n");

	// Test 1: punching, round=1, firewall=CONSISTENT
	{
		dht_msg::HolepunchPayload p;
		memset(&p, 0, sizeof(p));
		p.firewall = 2;
		p.round = 1;
		p.punching = true;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::holepunch_payload_preencode(s, p);
		EXPECT_EQ("punching preencode size", s.end, 4);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::holepunch_payload_encode(s, p);
		EXPECT_HEX("punching encode", buf, s.start, "02000201");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::HolepunchPayload dp;
		dht_msg::holepunch_payload_decode(ds, dp);
		EXPECT_TRUE("punching decode punching", dp.punching);
		EXPECT_TRUE("punching decode not connected", !dp.connected);
		EXPECT_EQ("punching decode firewall", dp.firewall, 2u);
		EXPECT_EQ("punching decode round", dp.round, 1u);
	}

	// Test 2: with addresses, remoteAddress, tokens
	{
		dht_msg::HolepunchPayload p;
		memset(&p, 0, sizeof(p));
		p.firewall = 1;
		p.round = 3;
		p.punching = true;
		p.addresses[0] = { {10, 0, 0, 1}, 12345 };
		p.addresses[1] = { {192, 168, 1, 1}, 4242 };
		p.addresses_count = 2;
		p.has_remote_address = true;
		p.remote_address = { {88, 99, 3, 86}, 49737 };
		p.has_token = true;
		memset(p.token, 0xCC, 32);
		p.has_remote_token = true;
		memset(p.remote_token, 0xDD, 32);

		EncodeState s = { 0, 0, nullptr };
		dht_msg::holepunch_payload_preencode(s, p);
		EXPECT_EQ("full payload preencode size", s.end, 87);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::holepunch_payload_encode(s, p);
		EXPECT_HEX("full payload encode", buf, s.start,
			"3e000103020a0000013930c0a8010192105863035649c2ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");

		EXPECT_EQ("full payload consumed all", s.start, s.end);
	}
}

// ---- Test PeerRecord encode/decode ----
static void test_peer_record() {
	printf("\n=== PeerRecord encoding ===\n");

	// Test 1: no relay addresses
	{
		dht_msg::PeerRecord p;
		memset(&p, 0, sizeof(p));
		for (int i = 0; i < 32; i++) p.public_key[i] = i;
		p.relay_count = 0;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::peer_record_preencode(s, p);
		EXPECT_EQ("no relay preencode size", s.end, 33);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::peer_record_encode(s, p);
		EXPECT_HEX("no relay encode", buf, s.start,
			"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::PeerRecord dp;
		dht_msg::peer_record_decode(ds, dp);
		EXPECT_EQ("no relay decode relay_count", dp.relay_count, 0);
		EXPECT_TRUE("no relay decode pk[0]", dp.public_key[0] == 0);
		EXPECT_TRUE("no relay decode pk[31]", dp.public_key[31] == 31);
	}

	// Test 2: with 2 relay addresses
	{
		dht_msg::PeerRecord p;
		memset(&p, 0, sizeof(p));
		memset(p.public_key, 0xFF, 32);
		p.relay_addresses[0] = { {88, 99, 3, 86}, 49737 };
		p.relay_addresses[1] = { {142, 93, 90, 113}, 49737 };
		p.relay_count = 2;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::peer_record_preencode(s, p);
		EXPECT_EQ("with relays preencode size", s.end, 45);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::peer_record_encode(s, p);
		EXPECT_HEX("with relays encode", buf, s.start,
			"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff025863035649c28e5d5a7149c2");

		EXPECT_EQ("with relays consumed all", s.start, s.end);
	}
}

// ---- Test AnnounceMessage encode/decode ----
static void test_announce_message() {
	printf("\n=== AnnounceMessage encoding ===\n");

	// Test 1: peer + signature, no refresh
	{
		dht_msg::AnnounceMessage m;
		memset(&m, 0, sizeof(m));
		m.has_peer = true;
		for (int i = 0; i < 32; i++) m.peer.public_key[i] = i;
		m.peer.relay_count = 0;
		m.has_signature = true;
		for (int i = 0; i < 64; i++) m.signature[i] = i;

		EncodeState s = { 0, 0, nullptr };
		dht_msg::announce_preencode(s, m);
		EXPECT_EQ("peer+sig preencode size", s.end, 98);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::announce_encode(s, m);
		EXPECT_HEX("peer+sig encode", buf, s.start,
			"05000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");

		// Decode round-trip
		EncodeState ds = { 0, s.start, buf };
		dht_msg::AnnounceMessage dm;
		dht_msg::announce_decode(ds, dm);
		EXPECT_TRUE("peer+sig decode has_peer", dm.has_peer);
		EXPECT_TRUE("peer+sig decode has_sig", dm.has_signature);
		EXPECT_TRUE("peer+sig decode no refresh", !dm.has_refresh);
		EXPECT_EQ("peer+sig consumed all", ds.start, ds.end);
	}

	// Test 2: peer + refresh + signature with relay
	{
		dht_msg::AnnounceMessage m;
		memset(&m, 0, sizeof(m));
		m.has_peer = true;
		memset(m.peer.public_key, 0xAA, 32);
		m.peer.relay_addresses[0] = { {88, 99, 3, 86}, 49737 };
		m.peer.relay_count = 1;
		m.has_refresh = true;
		memset(m.refresh, 0xBB, 32);
		m.has_signature = true;
		memset(m.signature, 0xCC, 64);

		EncodeState s = { 0, 0, nullptr };
		dht_msg::announce_preencode(s, m);
		EXPECT_EQ("peer+refresh+sig preencode size", s.end, 136);

		uint8_t buf[256];
		s.buffer = buf;
		s.start = 0;
		dht_msg::announce_encode(s, m);
		EXPECT_HEX("peer+refresh+sig encode", buf, s.start,
			"07aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa015863035649c2bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc");

		EXPECT_EQ("peer+refresh+sig consumed all", s.start, s.end);
	}
}

// ---- Test SecurePayload encrypt/decrypt ----
static void test_secure_payload() {
	printf("\n=== SecurePayload encrypt/decrypt ===\n");

	// Test with deterministic nonce: verify ciphertext matches JS
	{
		uint8_t key[32];
		memset(key, 0x42, 32);

		const char *plaintext_hex = "686f6c6570756e63682d746573742d7061796c6f6164";
		uint8_t plaintext[256];
		size_t pt_len;
		hex_to_bytes(plaintext_hex, plaintext, &pt_len);

		uint8_t nonce[24];
		for (int i = 0; i < 24; i++) nonce[i] = i;

		// Manual encrypt with known nonce (can't use secure_payload_encrypt which uses random nonce)
		uint8_t ct[256];
		crypto_secretbox_easy(ct, plaintext, pt_len, nonce, key);

		size_t ct_len = pt_len + crypto_secretbox_MACBYTES;
		uint8_t encrypted[256];
		memcpy(encrypted, nonce, 24);
		memcpy(encrypted + 24, ct, ct_len);
		size_t enc_len = 24 + ct_len;

		EXPECT_EQ("secure payload encrypted len", enc_len, 62u);
		EXPECT_HEX("secure payload encrypted", encrypted, enc_len,
			"000102030405060708090a0b0c0d0e0f1011121314151617130ad1bffa34ebf527ed76e57b63d67a0d41b99a15fc8b7782bf0826741252b4a04bb664baf5");

		// Decrypt using our function
		std::vector<uint8_t> decrypted;
		bool ok = dht_msg::secure_payload_decrypt(encrypted, enc_len, key, decrypted);
		EXPECT_TRUE("secure payload decrypt ok", ok);
		EXPECT_EQ("secure payload decrypt len", decrypted.size(), pt_len);
		if (decrypted.size() == pt_len) {
			EXPECT_TRUE("secure payload decrypt data",
				memcmp(decrypted.data(), plaintext, pt_len) == 0);
		}

		// Test round-trip with random nonce
		auto enc_random = dht_msg::secure_payload_encrypt(plaintext, pt_len, key);
		EXPECT_EQ("secure payload random enc len", enc_random.size(), 62u);
		std::vector<uint8_t> dec_random;
		ok = dht_msg::secure_payload_decrypt(enc_random.data(), enc_random.size(), key, dec_random);
		EXPECT_TRUE("secure payload random roundtrip", ok);
		EXPECT_TRUE("secure payload random data",
			dec_random.size() == pt_len && memcmp(dec_random.data(), plaintext, pt_len) == 0);
	}
}

// ---- Test Ed25519 keypair generation from seed ----
static void test_ed25519_keypair() {
	printf("\n=== Ed25519 keypair from seed ===\n");

	// Initiator seed: 0x00..0x1f
	{
		uint8_t seed[32];
		for (int i = 0; i < 32; i++) seed[i] = i;

		Ed25519KeyPair kp;
		crypto_sign_seed_keypair(kp.public_key, kp.secret_key, seed);

		EXPECT_HEX("initiator pk", kp.public_key, 32,
			"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8");
		EXPECT_HEX("initiator sk", kp.secret_key, 64,
			"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8");
	}

	// Responder seed: 0x20..0x3f
	{
		uint8_t seed[32];
		for (int i = 0; i < 32; i++) seed[i] = 0x20 + i;

		Ed25519KeyPair kp;
		crypto_sign_seed_keypair(kp.public_key, kp.secret_key, seed);

		EXPECT_HEX("responder pk", kp.public_key, 32,
			"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7");
		EXPECT_HEX("responder sk", kp.secret_key, 64,
			"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7");
	}
}

// ---- Test Noise IK handshake internal consistency ----
static void test_noise_ik_handshake() {
	printf("\n=== Noise IK handshake (internal consistency) ===\n");

	// Use deterministic keypairs
	uint8_t iseed[32], rseed[32];
	for (int i = 0; i < 32; i++) iseed[i] = i;
	for (int i = 0; i < 32; i++) rseed[i] = 0x20 + i;

	Ed25519KeyPair ikp, rkp;
	crypto_sign_seed_keypair(ikp.public_key, ikp.secret_key, iseed);
	crypto_sign_seed_keypair(rkp.public_key, rkp.secret_key, rseed);

	// Namespace
	uint8_t ids[] = { 4, 5, 6, 0, 1 };
	uint8_t ns_out[5 * 32];
	NoiseIK::compute_namespace("hyperswarm/dht", ids, 5, ns_out);
	const uint8_t *ns_handshake = ns_out + 3 * 32;
	const uint8_t *ns_holepunch = ns_out + 4 * 32;

	// Create initiator and responder
	NoiseIK initiator(true, ikp, ns_handshake, rkp.public_key);
	NoiseIK responder(false, rkp, ns_handshake, nullptr);

	// Message 1: initiator → responder
	auto msg1 = initiator.send();
	EXPECT_TRUE("msg1 non-empty", msg1.size() > 0);
	// Msg1 = e(32) + encrypted_s(32+16) + encrypted_payload(0+16) = 96 bytes
	EXPECT_EQ("msg1 size (no payload)", msg1.size(), 96u);
	EXPECT_TRUE("initiator not complete after msg1", !initiator.is_complete());

	// Responder receives msg1
	auto payload1 = responder.recv(msg1.data(), msg1.size());
	EXPECT_TRUE("responder recv msg1 ok", true); // If it didn't crash

	// Message 2: responder → initiator
	auto msg2 = responder.send();
	EXPECT_TRUE("msg2 non-empty", msg2.size() > 0);
	// Msg2 = e(32) + encrypted_payload(0+16) = 48 bytes
	EXPECT_EQ("msg2 size (no payload)", msg2.size(), 48u);
	EXPECT_TRUE("responder complete after msg2", responder.is_complete());

	// Initiator receives msg2
	auto payload2 = initiator.recv(msg2.data(), msg2.size());
	EXPECT_TRUE("initiator complete after recv msg2", initiator.is_complete());

	// Verify derived keys: initiator's tx = responder's rx, initiator's rx = responder's tx
	EXPECT_TRUE("tx/rx key crossover",
		memcmp(initiator.tx_key(), responder.rx_key(), 32) == 0 &&
		memcmp(initiator.rx_key(), responder.tx_key(), 32) == 0);

	// Verify handshake hashes match
	EXPECT_TRUE("handshake hashes match",
		memcmp(initiator.handshake_hash(), responder.handshake_hash(), 64) == 0);

	// Verify remote public keys
	EXPECT_TRUE("initiator sees responder pk",
		memcmp(initiator.remote_public_key(), rkp.public_key, 32) == 0);
	EXPECT_TRUE("responder sees initiator pk",
		memcmp(responder.remote_public_key(), ikp.public_key, 32) == 0);

	// Derive holepunch secrets
	uint8_t i_hp[32], r_hp[32];
	initiator.derive_holepunch_secret(ns_holepunch, i_hp);
	responder.derive_holepunch_secret(ns_holepunch, r_hp);
	EXPECT_TRUE("holepunch secrets match",
		memcmp(i_hp, r_hp, 32) == 0);
}

// ---- Test Noise IK handshake with payloads ----
static void test_noise_ik_with_payload() {
	printf("\n=== Noise IK handshake with payloads ===\n");

	auto ikp = NoiseIK::generate_keypair();
	auto rkp = NoiseIK::generate_keypair();

	uint8_t ids[] = { 4, 5, 6, 0, 1 };
	uint8_t ns_out[5 * 32];
	NoiseIK::compute_namespace("hyperswarm/dht", ids, 5, ns_out);
	const uint8_t *ns_handshake = ns_out + 3 * 32;

	// Initiator sends a NoisePayload in msg1
	dht_msg::NoisePayload np;
	memset(&np, 0, sizeof(np));
	np.version = 1;
	np.firewall = 2;
	np.addresses4[0] = { {10, 0, 0, 1}, 12345 };
	np.addresses4_count = 1;

	EncodeState ps = { 0, 0, nullptr };
	dht_msg::noise_payload_preencode(ps, np);
	uint8_t payload_buf[256];
	ps.buffer = payload_buf;
	ps.start = 0;
	dht_msg::noise_payload_encode(ps, np);
	size_t payload_len = ps.start;

	NoiseIK initiator(true, ikp, ns_handshake, rkp.public_key);
	NoiseIK responder(false, rkp, ns_handshake, nullptr);

	auto msg1 = initiator.send(payload_buf, payload_len);
	EXPECT_EQ("msg1 with payload size", msg1.size(), 96u + payload_len);

	auto dec_payload = responder.recv(msg1.data(), msg1.size());
	EXPECT_EQ("decoded payload len", dec_payload.size(), payload_len);

	// Decode the NoisePayload from the decrypted data
	EncodeState ds = { 0, dec_payload.size(), dec_payload.data() };
	dht_msg::NoisePayload dp;
	dht_msg::noise_payload_decode(ds, dp);
	EXPECT_EQ("payload firewall", dp.firewall, 2u);
	EXPECT_EQ("payload addr count", dp.addresses4_count, 1);
	EXPECT_EQ("payload addr port", dp.addresses4[0].port, 12345u);

	// Complete the handshake
	auto msg2 = responder.send();
	auto dec2 = initiator.recv(msg2.data(), msg2.size());
	EXPECT_TRUE("handshake complete", initiator.is_complete() && responder.is_complete());
	EXPECT_TRUE("keys crossover",
		memcmp(initiator.tx_key(), responder.rx_key(), 32) == 0);
}

int main() {
	if (sodium_init() < 0) {
		printf("sodium_init failed\n");
		return 1;
	}

	test_namespaces();
	test_noise_payload();
	test_handshake_message();
	test_holepunch_payload();
	test_peer_record();
	test_announce_message();
	test_secure_payload();
	test_ed25519_keypair();
	test_noise_ik_handshake();
	test_noise_ik_with_payload();

	printf("\n=== Results: %d/%d passed ===\n", tests_passed, tests_run);
	return tests_passed == tests_run ? 0 : 1;
}