OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_compact_encoding.cpp

586 lines · cpp ref:
// Test compact encoding wire compatibility with JS reference.
// Build: g++ -std=c++17 -I../src -lsodium -o test_compact_encoding test_compact_encoding.cpp
// Run: ./test_compact_encoding

#include "compact_encoding.h"
#include "kademlia_table.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';
}

static bool check_hex(const char *name, const uint8_t *actual, size_t actual_len,
					   const char *expected_hex) {
	tests_run++;
	uint8_t expected[512];
	size_t expected_len;
	hex_to_bytes(expected_hex, expected, &expected_len);

	if (actual_len != expected_len || memcmp(actual, expected, actual_len) != 0) {
		char actual_hex[1024];
		bytes_to_hex(actual, actual_len, actual_hex);
		printf("FAIL %s: expected %s, got %s\n", name, expected_hex, actual_hex);
		return false;
	}

	tests_passed++;
	printf("PASS %s\n", name);
	return true;
}

// ---- Encode helper ----

static size_t encode_uint(uint8_t *buf, uint64_t val) {
	EncodeState s = { 0, 0, nullptr };
	cenc::uint_preencode(s, val);
	s.buffer = buf;
	s.start = 0;
	cenc::uint_encode(s, val);
	return s.start;
}

static size_t encode_uint16(uint8_t *buf, uint16_t val) {
	EncodeState s = { 0, 0, buf };
	cenc::uint16_encode(s, val);
	return 2;
}

static size_t encode_uint32(uint8_t *buf, uint32_t val) {
	EncodeState s = { 0, 0, buf };
	cenc::uint32_encode(s, val);
	return 4;
}

static size_t encode_ipv4_address(uint8_t *buf, const IPv4Address &addr) {
	EncodeState s = { 0, 0, buf };
	cenc::ipv4_address_encode(s, addr);
	return 6;
}

static size_t encode_buffer(uint8_t *buf, const uint8_t *data, size_t len) {
	EncodeState s = { 0, 0, nullptr };
	cenc::buffer_preencode(s, data, len);
	s.buffer = buf;
	s.start = 0;
	cenc::buffer_encode(s, data, len);
	return s.start;
}

// ---- Tests ----

void test_uint_encoding() {
	uint8_t buf[16];
	size_t len;

	// From test vectors:
	struct { uint64_t val; const char *hex; } cases[] = {
		{ 0, "00" },
		{ 1, "01" },
		{ 42, "2a" },
		{ 252, "fc" },
		{ 253, "fdfd00" },
		{ 4200, "fd6810" },
		{ 65535, "fdffff" },
		{ 65536, "fe00000100" },
		{ 305419896, "fe78563412" },
		{ 4294967295ULL, "feffffffff" },
	};

	for (auto &c : cases) {
		len = encode_uint(buf, c.val);
		char name[64];
		snprintf(name, sizeof(name), "uint(%llu)", (unsigned long long)c.val);
		check_hex(name, buf, len, c.hex);
	}

	// Roundtrip decode.
	for (auto &c : cases) {
		uint8_t expected[16];
		size_t exp_len;
		hex_to_bytes(c.hex, expected, &exp_len);

		EncodeState s = { 0, exp_len, expected };
		uint64_t decoded = cenc::uint_decode(s);

		tests_run++;
		if (decoded == c.val) {
			printf("PASS uint_decode(%llu)\n", (unsigned long long)c.val);
			tests_passed++;
		} else {
			printf("FAIL uint_decode(%llu): got %llu\n",
				(unsigned long long)c.val, (unsigned long long)decoded);
		}
	}
}

void test_uint16_encoding() {
	uint8_t buf[4];

	struct { uint16_t val; const char *hex; } cases[] = {
		{ 0, "0000" },
		{ 1, "0100" },
		{ 256, "0001" },
		{ 49737, "49c2" },
		{ 65535, "ffff" },
	};

	for (auto &c : cases) {
		encode_uint16(buf, c.val);
		char name[64];
		snprintf(name, sizeof(name), "uint16(%u)", c.val);
		check_hex(name, buf, 2, c.hex);
	}
}

void test_uint32_encoding() {
	uint8_t buf[8];

	struct { uint32_t val; const char *hex; } cases[] = {
		{ 0, "00000000" },
		{ 1, "01000000" },
		{ 65536, "00000100" },
		{ 305419896, "78563412" },
		{ 4294967295U, "ffffffff" },
	};

	for (auto &c : cases) {
		encode_uint32(buf, c.val);
		char name[64];
		snprintf(name, sizeof(name), "uint32(%u)", c.val);
		check_hex(name, buf, 4, c.hex);
	}
}

void test_ipv4_address_encoding() {
	uint8_t buf[8];

	struct { const char *ip_port; const char *hex; } cases[] = {
		{ "88.99.3.86:49737", "5863035649c2" },
		{ "142.93.90.113:49737", "8e5d5a7149c2" },
		{ "138.68.147.8:49737", "8a44930849c2" },
		{ "192.168.1.1:4242", "c0a801019210" },
		{ "0.0.0.0:0", "000000000000" },
		{ "255.255.255.255:65535", "ffffffffffff" },
	};

	for (auto &c : cases) {
		IPv4Address addr;
		cenc::ipv4_address_parse(c.ip_port, addr);
		size_t len = encode_ipv4_address(buf, addr);
		char name[64];
		snprintf(name, sizeof(name), "ipv4Address(%s)", c.ip_port);
		check_hex(name, buf, len, c.hex);
	}
}

void test_node_id() {
	struct { const char *ip_port; const char *hex; } cases[] = {
		{ "88.99.3.86:49737", "54d4b9c6441048061f80cf69564cee46a5730adfcd59fbe6c056bb49af771546" },
		{ "142.93.90.113:49737", "52a9b67d9cf90cebefe2c5bc0122c2b765359b8429f9b610afd4c770c29330dc" },
		{ "138.68.147.8:49737", "e1dd7eba63d1e623c693691de6e34bbc53d5e941e5e1581948476aff4c9ec327" },
		{ "192.168.1.1:4242", "60bcb0a65d7107dd8df1dd7b587e343420c1c2e1f10511e3b98ab025618cde0d" },
	};

	for (auto &c : cases) {
		IPv4Address addr;
		cenc::ipv4_address_parse(c.ip_port, addr);
		NodeId nid = cenc::node_id_from_address(addr);
		char name[64];
		snprintf(name, sizeof(name), "nodeId(%s)", c.ip_port);
		check_hex(name, nid.bytes, 32, c.hex);
	}
}

void test_buffer_encoding() {
	uint8_t buf[512];
	size_t len;

	// null buffer.
	len = encode_buffer(buf, nullptr, 0);
	check_hex("buffer(null)", buf, len, "00");

	// "hello" (5 bytes).
	const uint8_t hello[] = { 'h', 'e', 'l', 'l', 'o' };
	len = encode_buffer(buf, hello, 5);
	check_hex("buffer(hello)", buf, len, "0568656c6c6f");

	// 200 bytes of 0xAB (length 200 = 0xC8, fits in 1-byte varint).
	uint8_t data200[200];
	memset(data200, 0xAB, 200);
	len = encode_buffer(buf, data200, 200);
	// Expected: 0xC8 followed by 200 bytes of 0xAB.
	tests_run++;
	if (len == 201 && buf[0] == 0xC8) {
		bool ok = true;
		for (int i = 0; i < 200; i++) {
			if (buf[i + 1] != 0xAB) { ok = false; break; }
		}
		if (ok) {
			printf("PASS buffer(200 bytes)\n");
			tests_passed++;
		} else {
			printf("FAIL buffer(200 bytes): wrong data\n");
		}
	} else {
		printf("FAIL buffer(200 bytes): len=%zu, first=0x%02x\n", len, buf[0]);
	}
}

void test_dht_request_encoding() {
	// FIND_NODE request with HAS_ID and TARGET.
	// From test vectors:
	const char *expected = "030d2a005863035649c2"
		"b64569e195f5990df77fb8f451642d8c34da292db02efd7bac308a28b8b9439d"
		"02"
		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";

	// Build the message manually (matching io.js _encodeRequest).
	uint8_t buf[256];
	EncodeState s = { 0, 0, buf };

	// Compute sender ID from 10.0.0.1:12345.
	IPv4Address sender;
	cenc::ipv4_address_parse("10.0.0.1:12345", sender);
	NodeId sender_id = cenc::node_id_from_address(sender);

	IPv4Address to;
	cenc::ipv4_address_parse("88.99.3.86:49737", to);

	uint8_t target[32];
	for (int i = 0; i < 32; i++) target[i] = static_cast<uint8_t>(i);

	s.buffer[s.start++] = 0x03; // REQUEST_ID
	s.buffer[s.start++] = 0x0D; // flags: HAS_ID|INTERNAL|TARGET
	cenc::uint16_encode(s, 42);  // tid
	cenc::ipv4_address_encode(s, to);
	cenc::fixed32_encode(s, sender_id.bytes); // ID
	cenc::uint_encode(s, 2);     // command: FIND_NODE
	cenc::fixed32_encode(s, target);

	check_hex("dht_request(FIND_NODE)", buf, s.start, expected);
}

void test_dht_response_encoding() {
	// Response with HAS_ID, TOKEN, CLOSER_NODES.
	const char *expected = "13072a000a000001393054d4b9c6441048061f80cf69564cee46a5730adfcd59fbe6c056bb49af771546"
		"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
		"028e5d5a7149c28a44930849c2";

	uint8_t buf[256];
	EncodeState s = { 0, 0, buf };

	IPv4Address to;
	cenc::ipv4_address_parse("10.0.0.1:12345", to);

	IPv4Address responder_addr;
	cenc::ipv4_address_parse("88.99.3.86:49737", responder_addr);
	NodeId responder_id = cenc::node_id_from_address(responder_addr);

	uint8_t token[32];
	memset(token, 0xFF, 32);

	IPv4Address closer[2];
	cenc::ipv4_address_parse("142.93.90.113:49737", closer[0]);
	cenc::ipv4_address_parse("138.68.147.8:49737", closer[1]);

	s.buffer[s.start++] = 0x13; // RESPONSE_ID
	s.buffer[s.start++] = 0x07; // flags: HAS_ID|TOKEN|CLOSER_NODES
	cenc::uint16_encode(s, 42);
	cenc::ipv4_address_encode(s, to);
	cenc::fixed32_encode(s, responder_id.bytes);
	cenc::fixed32_encode(s, token);
	cenc::ipv4_array_encode(s, closer, 2);

	check_hex("dht_response(CLOSER_NODES)", buf, s.start, expected);
}

void test_kademlia_table() {
	KademliaTable table;

	// Create a self ID.
	IPv4Address self_addr;
	cenc::ipv4_address_parse("10.0.0.1:12345", self_addr);
	NodeId self_id = cenc::node_id_from_address(self_addr);
	table.init(self_id);

	// Add some nodes.
	IPv4Address addrs[] = {
		{}, {}, {}, {}, {}
	};
	cenc::ipv4_address_parse("88.99.3.86:49737", addrs[0]);
	cenc::ipv4_address_parse("142.93.90.113:49737", addrs[1]);
	cenc::ipv4_address_parse("138.68.147.8:49737", addrs[2]);
	cenc::ipv4_address_parse("192.168.1.1:4242", addrs[3]);
	cenc::ipv4_address_parse("10.0.0.2:5000", addrs[4]);

	for (int i = 0; i < 5; i++) {
		KademliaNode kn;
		kn.id = cenc::node_id_from_address(addrs[i]);
		kn.address = addrs[i];
		kn.added_at = 0;
		table.add(kn);
	}

	tests_run++;
	if (table.size() == 5) {
		printf("PASS kademlia_add(5 nodes)\n");
		tests_passed++;
	} else {
		printf("FAIL kademlia_add: size=%d expected 5\n", table.size());
	}

	// Find closest to a random target.
	uint8_t target[32];
	memset(target, 0x42, 32);
	KademliaNode closest[5];
	int n = table.closest(target, closest, 5);

	tests_run++;
	if (n == 5) {
		printf("PASS kademlia_closest(5)\n");
		tests_passed++;
	} else {
		printf("FAIL kademlia_closest: got %d expected 5\n", n);
	}

	// Remove a node.
	NodeId remove_id = cenc::node_id_from_address(addrs[2]);
	bool removed = table.remove(remove_id.bytes);
	tests_run++;
	if (removed && table.size() == 4) {
		printf("PASS kademlia_remove\n");
		tests_passed++;
	} else {
		printf("FAIL kademlia_remove: removed=%d size=%d\n", removed, table.size());
	}

	// Has/get.
	NodeId lookup_id = cenc::node_id_from_address(addrs[0]);
	tests_run++;
	if (table.has(lookup_id.bytes)) {
		printf("PASS kademlia_has\n");
		tests_passed++;
	} else {
		printf("FAIL kademlia_has\n");
	}
}

void test_xor_distance() {
	NodeId a, b, target;
	memset(a.bytes, 0, 32);
	memset(b.bytes, 0, 32);
	memset(target.bytes, 0, 32);

	a.bytes[0] = 0x10; // Distance to 0: bit 3.
	b.bytes[0] = 0x20; // Distance to 0: bit 2.

	// b is farther from 0 than a (0x20 > 0x10 XOR 0x00).
	tests_run++;
	int cmp = cenc::xor_compare(target, a, b);
	if (cmp < 0) {
		printf("PASS xor_compare(a closer than b)\n");
		tests_passed++;
	} else {
		printf("FAIL xor_compare: expected <0, got %d\n", cmp);
	}

	// Distance bit.
	tests_run++;
	int bit = cenc::xor_distance_bit(a, target);
	if (bit == 3) {
		printf("PASS xor_distance_bit(0x10, 0) = 3\n");
		tests_passed++;
	} else {
		printf("FAIL xor_distance_bit: expected 3, got %d\n", bit);
	}
}

void test_ipv4_parse() {
	// Parse and format roundtrip.
	IPv4Address addr;
	cenc::ipv4_address_parse("192.168.1.1:4242", addr);

	tests_run++;
	if (addr.ip[0] == 192 && addr.ip[1] == 168 && addr.ip[2] == 1 && addr.ip[3] == 1 && addr.port == 4242) {
		printf("PASS ipv4_parse(192.168.1.1:4242)\n");
		tests_passed++;
	} else {
		printf("FAIL ipv4_parse: %d.%d.%d.%d:%d\n",
			addr.ip[0], addr.ip[1], addr.ip[2], addr.ip[3], addr.port);
	}

	char buf[32];
	cenc::ipv4_address_format(addr, buf, sizeof(buf));
	tests_run++;
	if (strcmp(buf, "192.168.1.1:4242") == 0) {
		printf("PASS ipv4_format\n");
		tests_passed++;
	} else {
		printf("FAIL ipv4_format: %s\n", buf);
	}
}

// ---- Truncated input: the contract a decoder owes its caller ----------------
//
// These are not wire-compatibility tests — no JS vector can describe them,
// because they are about what happens when the wire is WRONG. They exist to
// pin the contract down before src/compact_encoding.h delegates its byte work
// to libcompact:
//
//   1. a decoder never reads past `end`
//   2. a truncated decode leaves start == end, so a `while (start < end)` loop
//      over a malformed frame terminates instead of spinning
//   3. it yields a zero / null, never a half-read value the caller cannot
//      distinguish from a real one
//
// (3) is the one the hand-written codec does not fully honour: uint64_decode is
// two uint32 decodes, so four remaining bytes yield the low word rather than 0.
// Nothing depends on the value of a truncated decode, and "0 or null" is the
// contract worth having.

static bool check_true(const char *name, bool ok) {
	tests_run++;
	if (!ok) { printf("FAIL %s\n", name); return false; }
	tests_passed++;
	printf("PASS %s\n", name);
	return true;
}

static void test_truncated_decode() {
	printf("--- Truncated input ---\n");
	uint8_t buf[64];
	memset(buf, 0, sizeof(buf));

	{
		EncodeState s = { 0, 0, buf };
		check_true("uint_decode of nothing yields 0", cenc::uint_decode(s) == 0);
		check_true("  and does not move past the end", s.start == s.end);
	}
	{
		buf[0] = 0xfd; buf[1] = 0x01;
		EncodeState s = { 0, 2, buf };
		check_true("uint_decode of a truncated 0xfd yields 0", cenc::uint_decode(s) == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		buf[0] = 0xfe;
		EncodeState s = { 0, 4, buf };
		check_true("uint_decode of a truncated 0xfe yields 0", cenc::uint_decode(s) == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		buf[0] = 0xff;
		EncodeState s = { 0, 5, buf };
		check_true("uint_decode of a truncated 0xff yields 0", cenc::uint_decode(s) == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		EncodeState s = { 0, 1, buf };
		check_true("uint16_decode with one byte left yields 0", cenc::uint16_decode(s) == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		EncodeState s = { 0, 3, buf };
		check_true("uint32_decode with three bytes left yields 0", cenc::uint32_decode(s) == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		buf[0] = 1; buf[1] = 2; buf[2] = 3; buf[3] = 4;
		EncodeState s = { 0, 4, buf };
		const uint64_t v = cenc::uint64_decode(s);
		check_true("uint64_decode with half a word left yields 0, not the half", v == 0);
		check_true("  and clamps start to end", s.start == s.end);
		memset(buf, 0, sizeof(buf));
	}
	{
		EncodeState s = { 0, 31, buf };
		check_true("fixed32_decode short yields null", cenc::fixed32_decode(s) == nullptr);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		EncodeState s = { 0, 63, buf };
		check_true("fixed64_decode short yields null", cenc::fixed64_decode(s) == nullptr);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		buf[0] = 10;   // says ten bytes follow; three do
		EncodeState s = { 0, 4, buf };
		BufView v = cenc::buffer_decode(s);
		check_true("buffer_decode of a lying length yields nothing",
				   v.data == nullptr && v.len == 0);
		check_true("  and clamps start to end", s.start == s.end);
	}
	{
		buf[0] = 0;
		EncodeState s = { 0, 1, buf };
		BufView v = cenc::buffer_decode(s);
		check_true("an empty buffer decodes as null, not as a zero-length view",
				   v.data == nullptr && v.len == 0);
	}
	{
		buf[0] = 3; buf[1] = 0xaa; buf[2] = 0xbb; buf[3] = 0xcc;
		EncodeState s = { 0, 4, buf };
		BufView v = cenc::buffer_decode(s);
		check_true("a good buffer decodes ZERO-COPY, into the input",
				   v.len == 3 && v.data == buf + 1);
		check_true("  and consumes exactly its bytes", s.start == 4);
		memset(buf, 0, sizeof(buf));
	}
}

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

	printf("=== Compact Encoding Wire Compatibility Tests ===\n\n");

	test_uint_encoding();
	printf("\n");
	test_uint16_encoding();
	printf("\n");
	test_uint32_encoding();
	printf("\n");
	test_ipv4_address_encoding();
	printf("\n");
	test_node_id();
	printf("\n");
	test_buffer_encoding();
	printf("\n");
	test_ipv4_parse();
	printf("\n");
	test_xor_distance();
	printf("\n");
	test_kademlia_table();
	printf("\n");
	test_dht_request_encoding();
	printf("\n");
	test_dht_response_encoding();
	printf("\n");
	test_truncated_decode();

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