OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_unannounce_live.cpp

229 lines · cpp ref:
// 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;
}