OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_peer_topics.cpp

118 lines · cpp ref:
// Gate: a peer discovered on a SECOND topic must record it, even while we are
// already connected to that peer.
//
// docs/PARITY-hyperswarm.md §10. JS _handlePeer (hyperswarm/index.js:449)
// associates the topic BEFORE its already-connected early return:
//
//     const peerInfo = this._upsertPeer(...)
//     if (peerInfo) peerInfo._topic(topic)
//     if (!peerInfo || this._allConnections.has(peer.publicKey)) return
//
// Ours ran the association AFTER `if (info->peer_id != 0) return`, so the second
// topic was never recorded. That is not a cosmetic asymmetry: _should_requeue
// decides reconnection by walking exactly that list, so a peer found on A and B,
// connected via A, and dropped after A was left is requeued by JS and abandoned
// by us.
//
// RED before the fix: the peer is not requeued and the queue stays empty.

#include <cstdio>
#include <cstring>

#include <sodium.h>

#include "dht_connector.h"
#include "hyperdht.h"
#include "swarm_manager.h"

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

static int checks = 0, failures = 0;
static void check(bool cond, const char *what) {
	checks++;
	if (cond) { printf("  PASS: %s\n", what); return; }
	failures++;
	printf("  FAIL: %s\n", what);
}

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 kp;
	crypto_sign_keypair(kp.public_key, kp.secret_key);
	HyperDht dht;
	dht.init(&rpc, kp);
	DhtConnector connector;
	connector.init(&dht, &sock, &loop);

	SwarmManager swarm;
	swarm.init(&connector, &dht, kp.public_key);

	uint8_t topic_a[32], topic_b[32], peer_pk[32];
	memset(topic_a, 0xaa, 32);
	memset(topic_b, 0xbb, 32);
	randombytes_buf(peer_pk, 32);

	uint64_t now = 1000;
	swarm.tick(now);          // establish the clock before anything is timed

	swarm.join(topic_a, true, true);
	swarm.join(topic_b, true, true);

	// Discovered on A, then connected via A.
	swarm.on_peer_found(peer_pk, topic_a);
	const int32_t peer_id = 4242;
	swarm.on_peer_connected(peer_id, peer_pk, true);
	check(swarm.get_status().peers_connected >= 1, "connected via topic A");

	// ⛔ NOW DISCOVERED ON B, while already connected. JS records this; we did not.
	swarm.on_peer_found(peer_pk, topic_b);

	// Leave A. B is still joined, so the peer is still wanted.
	swarm.leave(topic_a);

	// Drop the connection. _should_requeue walks the peer's topics against the
	// joined set: if B was recorded it finds B and requeues; if not, it finds
	// nothing and the peer is abandoned.
	swarm.on_peer_disconnected(peer_id);

	// ⚠ A DISCONNECT DOES NOT QUEUE THE PEER DIRECTLY. on_peer_disconnected sets
	//   info->waiting = _should_requeue(info) && _retry.add(info)
	// so the peer enters the RETRY TIMER and only reaches the queue when that
	// timer fires on a later tick. The first version of this gate asserted
	// queue_size straight after the disconnect and failed against a working
	// fix — the assertion was wrong, not the code.
	bool requeued = false;
	for (int i = 0; i < 200 && !requeued; i++) {
		now += 100;
		swarm.tick(now);
		const SwarmStatus s = swarm.get_status();
		if (s.queue_size > 0 || s.connecting > 0) requeued = true;
	}

	check(requeued,
		  "the peer is requeued because topic B is still joined");

	printf("\n%d/%d passed\n", checks - failures, checks);

	udx_socket_close(&sock);
	uv_run(&loop, UV_RUN_NOWAIT);
	uv_loop_close(&loop);
	return failures ? 1 : 0;
}