// 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;
}