// Gate: after unannounce, NO node on a real testnet still holds our record.
//
// test_unannounce_live <host:port[,host:port...]>
// driven by test/run_unannounce_live.js, which stands up the testnet
//
// ⛔ THE MEASUREMENT IS "HOW MANY NODES STILL ANSWER WITH US", not "did
// unannounce return". A LOOKUP after the withdrawal reports one FoundPeer per
// answering node, tagged with the node that answered — so counting distinct
// answering nodes whose peer list holds our key is the quantity that matters,
// and nothing below it is evidence.
//
// It was written to settle docs/PARITY-hyperswarm.md A2, which claimed our
// commit-phase withdrawal reaches fewer nodes than JS's during-traversal one.
// It settled it the other way: see §7.5. The gate stays because "unannounce
// actually withdraws, from every node, on a real network" is worth holding on
// to whatever the answer to A2 turned out to be.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <set>
#include <string>
#include <vector>
#include <sodium.h>
#include "dht_rpc.h"
#include "hyperdht.h"
extern "C" {
#include <udx.h>
#include <uv.h>
}
enum Phase { BOOTSTRAPPING, ANNOUNCING, LOOKUP_BEFORE, UNANNOUNCING, LOOKUP_AFTER, DONE };
struct Ctx {
uv_loop_t loop;
udx_t udx;
udx_socket_t socket;
uv_timer_t timer;
uv_timer_t deadline;
DhtRpc rpc;
HyperDht dht;
uint8_t topic[32];
uint8_t our_pk[32];
Phase phase = BOOTSTRAPPING;
uint64_t phase_until = 0;
std::set<std::string> holders_before, holders_after;
std::set<std::string> *collecting = nullptr;
int phase_ms = 8000;
};
static std::string addr_key(const IPv4Address &a) {
char b[32];
snprintf(b, sizeof(b), "%d.%d.%d.%d:%d", a.ip[0], a.ip[1], a.ip[2], a.ip[3], a.port);
return b;
}
static void on_socket_recv(udx_socket_t *sock, ssize_t read_len, const uv_buf_t *buf,
const struct sockaddr *from) {
Ctx *ctx = static_cast<Ctx *>(sock->data);
if (!ctx || read_len <= 0) return;
const uint8_t *data = (const uint8_t *)buf->base;
if ((size_t)read_len >= 4 && (data[0] == DHT_REQUEST_ID || data[0] == DHT_RESPONSE_ID)) {
ctx->rpc.on_message(data, (size_t)read_len, (const struct sockaddr_in *)from);
}
}
static void on_tick(uv_timer_t *h) {
Ctx *ctx = static_cast<Ctx *>(h->data);
const uint64_t now = uv_now(&ctx->loop);
const int phase_ms = ctx->phase_ms;
ctx->rpc.tick(now);
ctx->dht.tick(now);
switch (ctx->phase) {
case BOOTSTRAPPING:
if (!ctx->rpc.is_bootstrapped()) break;
printf(" bootstrapped; announcing the topic\n");
ctx->dht.announce(ctx->topic);
ctx->phase = ANNOUNCING;
ctx->phase_until = now + phase_ms;
break;
case ANNOUNCING:
if (now < ctx->phase_until) break;
printf(" looking the topic up BEFORE the unannounce\n");
ctx->collecting = &ctx->holders_before;
ctx->dht.lookup(ctx->topic);
ctx->phase = LOOKUP_BEFORE;
ctx->phase_until = now + phase_ms;
break;
case LOOKUP_BEFORE:
if (now < ctx->phase_until) break;
printf(" %zu node(s) hold the record; unannouncing\n", ctx->holders_before.size());
ctx->collecting = nullptr;
ctx->dht.unannounce(ctx->topic);
ctx->phase = UNANNOUNCING;
ctx->phase_until = now + phase_ms + 4000;
break;
case UNANNOUNCING:
if (now < ctx->phase_until) break;
printf(" looking the topic up AFTER the unannounce\n");
ctx->collecting = &ctx->holders_after;
ctx->dht.lookup(ctx->topic);
ctx->phase = LOOKUP_AFTER;
ctx->phase_until = now + phase_ms + 4000;
break;
case LOOKUP_AFTER:
if (now < ctx->phase_until) break;
ctx->phase = DONE;
uv_stop(&ctx->loop);
break;
case DONE:
break;
}
}
static void on_deadline(uv_timer_t *h) {
uv_stop(&static_cast<Ctx *>(h->data)->loop);
}
int main(int argc, char **argv) {
if (sodium_init() < 0) return 1;
if (argc < 2) { printf("usage: %s <host:port,...> [phase_ms]\n", argv[0]); return 2; }
// A bigger testnet needs longer for an announce to settle and for a lookup
// to walk it. Passed in rather than guessed, because a phase that ends early
// reports "no holders" and that is indistinguishable from a real absence.
const int phase_ms = (argc > 2) ? atoi(argv[2]) : 8000;
std::vector<std::string> boots;
{
std::string all(argv[1]);
size_t p = 0;
while (p <= all.size()) {
size_t c = all.find(',', p);
if (c == std::string::npos) c = all.size();
if (c > p) boots.push_back(all.substr(p, c - p));
p = c + 1;
}
}
std::vector<const char *> bootv;
for (auto &b : boots) bootv.push_back(b.c_str());
printf(" bootstrap: %s\n", argv[1]);
Ctx *ctx = new Ctx();
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);
udx_socket_recv_start(&ctx->socket, on_socket_recv);
ctx->rpc.init(&ctx->socket, &ctx->loop);
ctx->rpc.set_bootstrap(bootv.data(), (int)bootv.size());
struct sockaddr_in local_addr;
int alen = sizeof(local_addr);
udx_socket_getsockname(&ctx->socket, (struct sockaddr *)&local_addr, &alen);
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);
Ed25519KeyPair kp;
crypto_sign_keypair(kp.public_key, kp.secret_key);
memcpy(ctx->our_pk, kp.public_key, 32);
ctx->dht.init(&ctx->rpc, kp);
ctx->phase_ms = phase_ms;
randombytes_buf(ctx->topic, 32);
// One FoundPeer per answering node per peer entry. We only care about the
// entries that are US, and about WHICH node answered with them.
ctx->dht.on_peer_found = [ctx](uint32_t, const FoundPeer &fp) {
if (!ctx->collecting) return;
if (memcmp(fp.public_key, ctx->our_pk, 32) != 0) return;
ctx->collecting->insert(addr_key(fp.relay_node));
};
ctx->rpc.bootstrap();
uv_timer_init(&ctx->loop, &ctx->timer);
ctx->timer.data = ctx;
uv_timer_start(&ctx->timer, on_tick, 0, 20);
uv_timer_init(&ctx->loop, &ctx->deadline);
ctx->deadline.data = ctx;
uv_timer_start(&ctx->deadline, on_deadline, (uint64_t)phase_ms * 6 + 40000, 0);
uv_run(&ctx->loop, UV_RUN_DEFAULT);
int checks = 0, failures = 0;
auto check = [&](bool c, const char *what) {
checks++;
if (c) { printf(" PASS: %s\n", what); return; }
failures++;
printf(" FAIL: %s\n", what);
};
printf("\n");
check(ctx->phase == DONE, "the whole sequence ran");
// ⛔ BASELINE. Without holders before, "no holders after" means nothing —
// it is the same shape as a gate reporting silence from a swarm that never
// meshed.
check(!ctx->holders_before.empty(),
"at least one node held the record BEFORE the unannounce");
check(ctx->holders_after.empty(),
"NO node still holds the record after the unannounce");
printf("\n holders before=%zu after=%zu\n",
ctx->holders_before.size(), ctx->holders_after.size());
if (!ctx->holders_after.empty()) {
printf(" still holding:");
for (const std::string &a : ctx->holders_after) printf(" %s", a.c_str());
printf("\n");
}
printf("\n%d/%d passed\n", checks - failures, checks);
return failures ? 1 : 0;
}