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