// Gate: a responder must still answer after it has already handled many
// handshakes. hyper-games #72's wall, reduced to one process.
//
// cross-machine, both peers fresh WORKS
// cross-machine, one peer up minutes FAILS <- this
//
// Mechanism under test: HyperDht keeps a FIXED pool of 16 DhtHandshake slots.
// _alloc_handshake() returns nullptr once _handshake_count hits 16, and
// _handle_handshake() answers that with a bare `return` -- no reply, no log.
// _free_handshake() has exactly ONE call site (the Noise-recv failure path), so
// a handshake that SUCCEEDS is never reclaimed and there is no timeout sweep.
// JS reclaims on stream close and on a clearing timeout (server.js _clearLater).
//
// What is counted: on_handshake_request, which HyperDht fires only AFTER it has
// allocated a slot and decrypted the payload, and immediately before it builds
// the reply. It is upstream of the send, so "callback fired" and "reply built"
// are the same event -- that is why counting it does not need a socket.
#include <cstdio>
#include <cstring>
#include <vector>
#include <sodium.h>
#include "dht_messages.h"
#include "dht_rpc.h"
#include "hyperdht.h"
#include "noise_ik.h"
extern "C" {
#include <udx.h>
#include <uv.h>
}
// How many handshakes to drive through one responder. Must exceed the pool.
static const int ATTEMPTS = 20;
static const int POOL = 16;
int main() {
if (sodium_init() < 0) { printf("sodium_init failed\n"); return 1; }
uv_loop_t loop;
uv_loop_init(&loop);
udx_t udx;
udx_init(&loop, &udx, nullptr);
udx_socket_t sock;
udx_socket_init(&udx, &sock, nullptr);
struct sockaddr_in addr;
uv_ip4_addr("127.0.0.1", 0, &addr);
udx_socket_bind(&sock, (struct sockaddr *)&addr, 0);
DhtRpc rpc;
rpc.init(&sock, &loop);
Ed25519KeyPair server_kp;
crypto_sign_keypair(server_kp.public_key, server_kp.secret_key);
HyperDht dht;
dht.init(&rpc, server_kp);
dht.listen();
int accepted = 0;
dht.on_handshake_request = [&accepted](uint32_t, const uint8_t *,
const dht_msg::NoisePayload &) {
accepted++;
};
// The namespace the peer handshake is bound to. Derived the same way the
// responder derives it, so the client's IK message is one it will accept.
uint8_t ns[32];
{
uint8_t ns_buf[5 * 32];
const char *name = "hyperswarm/dht";
uint8_t ids[5] = { 4, 5, 6, 0, 1 };
uint8_t base[32];
crypto_generichash(base, 32, (const uint8_t *)name, strlen(name), nullptr, 0);
for (int i = 0; i < 5; i++) {
crypto_generichash_state st;
crypto_generichash_init(&st, nullptr, 0, 32);
crypto_generichash_update(&st, base, 32);
crypto_generichash_update(&st, &ids[i], 1);
crypto_generichash_final(&st, ns_buf + i * 32, 32);
}
memcpy(ns, ns_buf + 3 * 32, 32); // PEER_HANDSHAKE = index 3 in that list
}
printf("driving %d handshakes into one responder (pool is %d)\n", ATTEMPTS, POOL);
// A daemon that has been up for a while: each handshake arrives a minute
// after the last, and the node ticks in between. That is the reported
// failure ("cross-machine, one peer up minutes"), not a burst — 16
// CONCURRENT handshakes is a capacity limit and a different question.
uint64_t now_ms = 1000;
for (int i = 0; i < ATTEMPTS; i++) {
now_ms += 60000;
dht.tick(now_ms);
// A DISTINCT client each time. Reusing one keypair would make every
// handshake identical and could be deduplicated for reasons unrelated
// to the pool, which would test the wrong thing.
Ed25519KeyPair client_kp;
crypto_sign_keypair(client_kp.public_key, client_kp.secret_key);
NoiseIK ik(true, client_kp, ns, server_kp.public_key);
dht_msg::NoisePayload payload;
memset(&payload, 0, sizeof(payload));
payload.version = 1;
payload.firewall = 1; // FIREWALL_OPEN
payload.has_udx = true;
payload.udx.id = 1000 + i;
payload.has_secret_stream = true;
EncodeState ps = { 0, 0, nullptr };
dht_msg::noise_payload_preencode(ps, payload);
std::vector<uint8_t> pbuf(ps.end);
ps.buffer = pbuf.data();
ps.start = 0;
dht_msg::noise_payload_encode(ps, payload);
auto noise_msg = ik.send(pbuf.data(), pbuf.size());
if (noise_msg.empty()) { printf(" client %d: IK send failed\n", i); break; }
HandshakeMessage hmsg;
memset(&hmsg, 0, sizeof(hmsg));
hmsg.mode = HS_MODE_FROM_CLIENT;
hmsg.noise = noise_msg.data();
hmsg.noise_len = noise_msg.size();
EncodeState hs_s = { 0, 0, nullptr };
dht_msg::handshake_preencode(hs_s, hmsg);
std::vector<uint8_t> hbuf(hs_s.end);
hs_s.buffer = hbuf.data();
hs_s.start = 0;
dht_msg::handshake_encode(hs_s, hmsg);
DhtRequest req;
memset(&req, 0, sizeof(req));
req.command = HYPERDHT_CMD_PEER_HANDSHAKE;
req.from.ip[0] = 127; req.from.ip[1] = 0; req.from.ip[2] = 0; req.from.ip[3] = 1;
req.from.port = (uint16_t)(40000 + i);
req.value = hbuf.data();
req.value_len = hbuf.size();
int before = accepted;
if (rpc.on_request) rpc.on_request(req);
if (accepted == before) printf(" handshake %2d: NO REPLY\n", i + 1);
}
printf("\naccepted %d of %d\n", accepted, ATTEMPTS);
const bool ok = (accepted == ATTEMPTS);
if (!ok) {
printf("FAIL: the responder stopped answering after %d handshakes.\n", accepted);
printf(" A daemon that has served this many goes permanently silent:\n");
printf(" _alloc_handshake returns nullptr and _handle_handshake returns\n");
printf(" without replying or logging. This is #72's wall.\n");
} else {
printf("PASS: the responder answered every handshake.\n");
}
udx_socket_close(&sock);
uv_run(&loop, UV_RUN_NOWAIT);
uv_loop_close(&loop);
return ok ? 0 : 1;
}