OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_self_announce_live.cpp

181 lines · cpp ref:
// Gate: against a REAL testnet, a listening peer records the nodes that took its
// announce and pings them.
//
//   test_self_announce_live <host:port[,host:port...]>
//   driven by test/run_self_announce_live.js, which stands up the testnet
//
// ⛔ WHY THIS EXISTS. test_self_announce.cpp is 10/10 and proves less than it
// looks like it does: it has no DHT, so nothing ever answers an announce,
// self_holder_count() is 0 throughout, and the recording, the ping round and the
// verdict are all exercised only in their EMPTY form. Every claim about the
// POPULATED path was untested. See docs/PARITY-hyperswarm.md §16.
//
// What this asserts, all of which requires real responders:
//   - holders are recorded from real commit replies (§15: they used to be
//     guessed from table().closest() with a zeroed token)
//   - the recorded token is NOT zero
//   - the ping round actually runs against them
//   - the holders answer, so the re-announce is NOT triggered spuriously

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#include <sodium.h>

#include "dht_rpc.h"
#include "hyperdht.h"

extern "C" {
#include <udx.h>
#include <uv.h>
}

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;
	bool listened = false;
	int holders_seen = 0;
	bool token_nonzero = false;
	uint64_t rounds_at_first_holder = 0;
	uint64_t announces_at_first_holder = 0;
	uint64_t stop_at = 0;
};

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);
	ctx->rpc.tick(now);
	ctx->dht.tick(now);

	if (!ctx->listened && ctx->rpc.is_bootstrapped()) {
		ctx->listened = true;
		printf("  bootstrapped, announcing self\n");
		ctx->dht.listen();
	}

	const int holders = ctx->dht.self_holder_count();
	if (holders > ctx->holders_seen) {
		if (ctx->holders_seen == 0) {
			ctx->rounds_at_first_holder = ctx->dht.self_ping_rounds();
			ctx->announces_at_first_holder = ctx->dht.self_announce_count();
			// Give the ping loop several rounds with holders present.
			ctx->stop_at = now + 12000;
			printf("  first holder recorded at %llums\n", (unsigned long long)now);
		}
		ctx->holders_seen = holders;
	}

	if (ctx->stop_at && now >= ctx->stop_at) uv_stop(&ctx->loop);
}

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,...>\n", argv[0]); return 2; }

	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);
	ctx->dht.init(&ctx->rpc, kp);

	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, 45000, 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->listened, "reached the testnet and announced");
	check(ctx->holders_seen > 0,
		  "recorded at least one node that ACCEPTED the announce");
	// ⛔ THE ONE THAT DISTINGUISHES THE FIX. The routing-table guess this
	// replaced also populated the node list — from table().closest() — so
	// "holders > 0" passes either way. It wrote `memset(token, 0, 32)`, so a
	// real token is the only externally visible difference.
	check(ctx->dht.self_holder_tokens() == ctx->holders_seen,
		  "every recorded holder carries a REAL token, not a zeroed one");
	check(ctx->dht.self_ping_rounds() > ctx->rounds_at_first_holder,
		  "ping rounds ran against the recorded holders");
	// Holders on a healthy testnet answer, so the failure path must NOT fire.
	check(ctx->dht.self_announce_count() <= ctx->announces_at_first_holder + 1,
		  "live holders answered, so no spurious re-announce storm");

	printf("\n  holders=%d tokens=%d ping_rounds=%llu announces=%llu\n",
		   ctx->holders_seen, ctx->dht.self_holder_tokens(),
		   (unsigned long long)ctx->dht.self_ping_rounds(),
		   (unsigned long long)ctx->dht.self_announce_count());
	printf("\n%d/%d passed\n", checks - failures, checks);
	return failures ? 1 : 0;
}