// swarm-probe — the smallest thing that uses hyperswarm-peer end to end.
//
// swarm-probe --topic WORDS [--name WHO] [--seconds N] [--bootstrap host:port]
//
// Join a topic, open a Protomux channel on every peer that appears, say hello,
// and exit 0 the moment a hello comes back. Nothing else: no Unix socket, no
// grab table, no Blender, no Godot.
//
// ⛔ IT EXISTS BECAUSE EVERY GATE THIS REPO HAS RUNS ON A TESTNET. Both
// cross-machine suites pass `--bootstrap` at a local testnet, so the one
// configuration the products actually use — two machines, the PUBLIC DHT — has
// no gate at all, and that is where the failures have lived (hyper-games #68
// bug A, and a whole evening of "joined but never connected"). The exit code is
// the point: 0 means a message crossed between two processes, and nothing else
// counts as the swarm working.
//
// ⚠ Hearing YOURSELF does not count. A probe that connects to its own announce
// would report success on one machine with no network at all, so the hello
// carries a name and a peer's hello must carry a different one.
#include "../src/udx_transport.h"
#include "../src/protomux.h"
#include "../src/noise_ik.h"
#include <sodium.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <csignal>
#include <mutex>
#include <string>
#include <unistd.h>
#include <vector>
static const char *PROBE_PROTOCOL = "hyperswarm-peer/probe/v1";
// ⛔ NOT atoi(). One call to it links __isoc23_strtol and raises this binary's
// glibc floor from 2.34 to 2.38 — measured: the probe built fine here and would
// not load on a 22.04 machine at all ("GLIBC_2.38 not found"), which is the same
// trap the ship-build note warns about, reached by one line of argument parsing.
static int parse_int(const char *s, int fallback) {
if (s == nullptr || *s == 0) return fallback;
int v = 0;
for (const char *p = s; *p; p++) {
if (*p < '0' || *p > '9') return fallback;
v = v * 10 + (*p - '0');
}
return v;
}
static std::atomic<bool> g_stop(false);
static void on_signal(int) { g_stop = true; }
struct Probe {
UdxTransport t;
std::vector<uint8_t> topic;
std::string name;
std::mutex m;
std::vector<ProtomuxChannel *> channels;
std::atomic<bool> heard{false};
std::string heard_from;
void on_mux(int32_t, Protomux *mux) {
if (!mux) return;
ProtomuxChannel *ch = mux->create_channel(PROBE_PROTOCOL, topic);
if (ch == nullptr) return; // already open on this mux
ch->add_message([this](const uint8_t *data, size_t len, ProtomuxChannel *) {
std::string who((const char *)data, len);
// ⚠ Our own hello echoed back is not evidence of a peer.
if (who == name) return;
{
std::lock_guard<std::mutex> lk(m);
heard_from = who;
}
heard = true;
});
ch->on_open = [this, ch](const uint8_t *, size_t) {
ch->send(0, (const uint8_t *)name.data(), name.size());
};
{
std::lock_guard<std::mutex> lk(m);
channels.push_back(ch);
}
ch->open();
}
// Say hello again on every pulse: the channel may have opened before the
// other side registered its handler, and a hello is 8 bytes.
void greet() {
std::lock_guard<std::mutex> lk(m);
for (auto *ch : channels) ch->send(0, (const uint8_t *)name.data(), name.size());
}
int peers() {
std::lock_guard<std::mutex> lk(m);
return (int)channels.size();
}
};
int main(int argc, char **argv) {
const char *seed = nullptr;
const char *bootstrap = nullptr;
std::string name = "probe-" + std::to_string(getpid());
int seconds = 90;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--topic") && i + 1 < argc) seed = argv[++i];
else if (!strcmp(argv[i], "--bootstrap") && i + 1 < argc) bootstrap = argv[++i];
else if (!strcmp(argv[i], "--name") && i + 1 < argc) name = argv[++i];
else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = parse_int(argv[++i], 90);
}
if (seed == nullptr) {
fprintf(stderr, "usage: swarm-probe --topic WORDS [--name WHO] [--seconds N] [--bootstrap host:port]\n");
return 2;
}
if (sodium_init() < 0) return 2;
Probe p;
p.name = name;
p.topic.resize(32);
crypto_generichash(p.topic.data(), 32, (const uint8_t *)seed, strlen(seed), nullptr, 0);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
p.t.set_mux_callbacks(
[&p](int32_t id, Protomux *mux) { p.on_mux(id, mux); },
[](int32_t) {});
Ed25519KeyPair kp = NoiseIK::generate_keypair();
const char *bs[1] = { bootstrap };
p.t.start_dht(kp, bs, 1);
printf("[probe] %s on topic %s\n", name.c_str(), seed);
fflush(stdout);
// ⛔ THE JOIN IS DEFERRED. start_dht() returns before the UV loop's async
// handle exists, and joining into that segfaults inside uv_async_send with
// no output at all. is_dht_active() is the signal it is safe.
bool joined = false;
int ticks = 0;
while (!g_stop && ticks < seconds * 5 && !p.heard) {
if (!joined && p.t.is_dht_active()) {
p.t.swarm_join(p.topic.data(), true, true);
joined = true;
}
if ((ticks % 25) == 0) {
printf("[probe] dht=%s peers=%d heard=%d\n",
p.t.is_dht_bootstrapped() ? "up" : (p.t.is_dht_active() ? "connecting" : "starting"),
p.peers(), p.heard ? 1 : 0);
fflush(stdout);
p.greet();
}
usleep(200 * 1000);
ticks++;
}
if (p.heard) {
printf("[probe] HEARD %s — the swarm works\n", p.heard_from.c_str());
fflush(stdout);
return 0;
}
printf("[probe] NOTHING HEARD after %ds (peers=%d)\n", seconds, p.peers());
fflush(stdout);
return 1;
}