// Test: DHT bootstrap using the same event loop style as UdxTransport.
// Reproduces the Godot integration pattern:
// - Thread with UV_RUN_NOWAIT + 500µs sleep
// - 1ms timer tick
// - DhtRpc tick'd from timer callback
// - HyperDht layer installed (overwrites on_response callback)
//
// This test isolates whether the Godot event loop pattern breaks bootstrap.
#include "compact_encoding.h"
#include "dht_rpc.h"
#include "hyperdht.h"
#include "kademlia_table.h"
#include "noise_ik.h"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <sodium.h>
#include <thread>
extern "C" {
#include <udx.h>
#include <uv.h>
}
// ---- Test context (mimics UdxTransport) ----
struct TestContext {
uv_loop_t loop;
udx_t udx;
udx_socket_t socket;
uv_timer_t tick_timer;
DhtRpc *rpc;
HyperDht *hyperdht;
std::atomic<bool> running;
std::atomic<bool> bootstrapped;
std::atomic<bool> init_requested;
int tick_count;
uint64_t start_time;
int table_size_at_bootstrap;
};
static void on_socket_recv(udx_socket_t *socket, ssize_t read_len,
const uv_buf_t *buf, const struct sockaddr *from) {
if (read_len <= 0) return;
TestContext *ctx = static_cast<TestContext *>(socket->data);
const uint8_t *data = (const uint8_t *)buf->base;
// Dispatch DHT messages (same as UdxTransport::_on_socket_recv).
if (ctx->rpc && (size_t)read_len >= 4) {
uint8_t type_byte = data[0];
if (type_byte == DHT_REQUEST_ID || type_byte == DHT_RESPONSE_ID) {
const struct sockaddr_in *from_in = (const struct sockaddr_in *)from;
ctx->rpc->on_message(data, (size_t)read_len, from_in);
}
}
}
static void on_tick(uv_timer_t *handle) {
TestContext *ctx = static_cast<TestContext *>(handle->data);
// Process init command (mimics _process_commands() handling CMD_DHT_INIT).
if (ctx->init_requested.load() && !ctx->rpc) {
ctx->init_requested.store(false);
printf(" [tick %d] Initializing DHT (uv_now=%lu)...\n",
ctx->tick_count, (unsigned long)uv_now(&ctx->loop));
// Create DHT-RPC (same as _init_dht).
ctx->rpc = new DhtRpc();
ctx->rpc->init(&ctx->socket, &ctx->loop);
const char *bootstrap[] = {
"88.99.3.86:49737",
"142.93.90.113:49737",
"138.68.147.8:49737",
};
ctx->rpc->set_bootstrap(bootstrap, 3);
// Set local address from bound socket.
struct sockaddr_in local_addr;
int addr_len = sizeof(local_addr);
udx_socket_getsockname(&ctx->socket, (struct sockaddr *)&local_addr, &addr_len);
IPv4Address local;
memcpy(local.ip, &local_addr.sin_addr.s_addr, 4);
local.port = ntohs(local_addr.sin_port);
ctx->rpc->set_local_address(local);
printf(" [tick %d] Local address: %d.%d.%d.%d:%d\n",
ctx->tick_count,
local.ip[0], local.ip[1], local.ip[2], local.ip[3], local.port);
// Create HyperDht layer (overwrites on_response callback, same as Godot).
Ed25519KeyPair kp;
memset(&kp, 0, sizeof(kp));
crypto_sign_keypair(kp.public_key, kp.secret_key);
ctx->hyperdht = new HyperDht();
ctx->hyperdht->init(ctx->rpc, kp);
// Set on_bootstrapped callback (after HyperDht::init, same order as Godot).
ctx->rpc->on_bootstrapped = [ctx]() {
ctx->bootstrapped.store(true);
ctx->table_size_at_bootstrap = ctx->rpc->table().size();
uint64_t elapsed = uv_now(&ctx->loop) - ctx->start_time;
printf(" [tick %d] on_bootstrapped fired! table=%d elapsed=%lu ms\n",
ctx->tick_count, ctx->table_size_at_bootstrap, (unsigned long)elapsed);
};
// Bootstrap!
ctx->start_time = uv_now(&ctx->loop);
ctx->rpc->bootstrap();
printf(" [tick %d] bootstrap() called, start_time=%lu\n",
ctx->tick_count, (unsigned long)ctx->start_time);
}
// Tick DHT (same as _process_commands() in UdxTransport).
if (ctx->rpc) {
uint64_t now = uv_now(&ctx->loop);
ctx->rpc->tick(now);
if (ctx->hyperdht) ctx->hyperdht->tick(now);
}
ctx->tick_count++;
// Stop after bootstrap or timeout.
if (ctx->bootstrapped.load() || ctx->tick_count > 15000) { // 15s max
ctx->running.store(false);
uv_timer_stop(&ctx->tick_timer);
uv_close((uv_handle_t *)&ctx->tick_timer, nullptr);
udx_socket_close(&ctx->socket);
}
}
int main() {
printf("=== DHT Bootstrap Test (Godot Style) ===\n");
printf("Tests bootstrap using UV_RUN_NOWAIT + 500us sleep + HyperDht layer\n\n");
if (sodium_init() < 0) {
printf("FAIL: sodium_init\n");
return 1;
}
TestContext ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.running.store(true);
ctx.bootstrapped.store(false);
ctx.init_requested.store(false);
ctx.tick_count = 0;
ctx.rpc = nullptr;
ctx.hyperdht = nullptr;
ctx.table_size_at_bootstrap = 0;
// Initialize libuv + libudx.
uv_loop_init(&ctx.loop);
udx_init(&ctx.loop, &ctx.udx, nullptr);
udx_socket_init(&ctx.udx, &ctx.socket, nullptr);
ctx.socket.data = &ctx;
struct sockaddr_in bind_addr;
uv_ip4_addr("0.0.0.0", 0, &bind_addr);
udx_socket_bind(&ctx.socket, (struct sockaddr *)&bind_addr, 0);
struct sockaddr_in local_addr;
int addr_len = sizeof(local_addr);
udx_socket_getsockname(&ctx.socket, (struct sockaddr *)&local_addr, &addr_len);
printf(" Bound to port %d\n", ntohs(local_addr.sin_port));
udx_socket_recv_start(&ctx.socket, on_socket_recv);
// Timer (1ms tick, same as Godot).
uv_timer_init(&ctx.loop, &ctx.tick_timer);
ctx.tick_timer.data = &ctx;
uv_timer_start(&ctx.tick_timer, on_tick, 0, 1);
// Simulate: request DHT init after some ticks (like Godot queuing CMD_DHT_INIT).
// In Godot, the thread starts, runs several ticks, THEN picks up the command.
// We simulate this by setting init_requested after a short delay.
ctx.init_requested.store(true);
printf(" Running event loop (UV_RUN_NOWAIT + 500us sleep)...\n");
// Run event loop Godot-style.
while (ctx.running.load()) {
uv_run(&ctx.loop, UV_RUN_NOWAIT);
std::this_thread::sleep_for(std::chrono::microseconds(500));
}
// Run final callbacks.
uv_run(&ctx.loop, UV_RUN_DEFAULT);
uv_loop_close(&ctx.loop);
// Results.
printf("\n --- Results ---\n");
printf(" Bootstrapped: %s\n", ctx.bootstrapped.load() ? "YES" : "NO");
if (ctx.rpc) {
printf(" Routing table nodes: %d\n", ctx.rpc->table().size());
}
printf(" Total ticks: %d\n", ctx.tick_count);
// Cleanup.
delete ctx.hyperdht;
delete ctx.rpc;
if (ctx.bootstrapped.load()) {
printf("\n PASS: DHT bootstrap with Godot-style event loop\n");
return 0;
} else {
printf("\n FAIL: DHT bootstrap did not complete\n");
return 1;
}
}