mirror of
https://github.com/logos-co/logos-libp2p-module.git
synced 2026-08-31 09:51:12 +00:00
310 lines
11 KiB
C++
310 lines
11 KiB
C++
#include <logos_test.h>
|
|
#include <plugin.h>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include "test_helpers.h"
|
|
|
|
static Libp2pModuleOptions discoOptions() {
|
|
return Libp2pModuleOptions{ .mountServiceDiscovery = true };
|
|
}
|
|
|
|
static std::unique_ptr<Libp2pModuleImpl> startDiscoPeer(
|
|
const std::string& bootstrapId, const std::vector<std::string>& bootstrapAddrs)
|
|
{
|
|
Libp2pModuleOptions opts = discoOptions();
|
|
opts.bootstrapNodes = { {bootstrapId, bootstrapAddrs} };
|
|
auto node = std::make_unique<Libp2pModuleImpl>(opts);
|
|
LOGOS_ASSERT_TRUE(node->start().success);
|
|
LOGOS_ASSERT_TRUE(node->discoStart().success);
|
|
LOGOS_ASSERT_TRUE(node->connectPeer(bootstrapId, bootstrapAddrs, 5000).success);
|
|
return node;
|
|
}
|
|
|
|
LOGOS_TEST(disco_start_stop) {
|
|
Libp2pModuleImpl node(discoOptions());
|
|
LOGOS_ASSERT_TRUE(node.start().success);
|
|
|
|
LOGOS_ASSERT_TRUE(node.discoStart().success);
|
|
LOGOS_ASSERT_TRUE(node.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(node.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_advertise_and_lookup) {
|
|
Libp2pModuleImpl nodeC(discoOptions());
|
|
LOGOS_ASSERT_TRUE(nodeC.start().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStart().success);
|
|
auto [peerIdC, addrsC] = getPeerInfoPair(nodeC);
|
|
|
|
auto nodeA = startDiscoPeer(peerIdC, addrsC);
|
|
auto nodeB = startDiscoPeer(peerIdC, addrsC);
|
|
|
|
std::string serviceId = "test-service";
|
|
LOGOS_ASSERT_TRUE(nodeA->discoStartAdvertising(serviceId, "", "").success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoRegisterInterest(serviceId).success);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
|
|
|
auto res = nodeB->discoLookup(serviceId, "");
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
|
|
auto records = res.value;
|
|
LOGOS_ASSERT_FALSE(records.empty());
|
|
|
|
auto [peerIdA, addrsA] = getPeerInfoPair(*nodeA);
|
|
LOGOS_ASSERT_TRUE(records[0]["peerId"].get<std::string>() == peerIdA);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_advertise_with_data) {
|
|
Libp2pModuleImpl nodeC(discoOptions());
|
|
LOGOS_ASSERT_TRUE(nodeC.start().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStart().success);
|
|
auto [peerIdC, addrsC] = getPeerInfoPair(nodeC);
|
|
|
|
auto nodeA = startDiscoPeer(peerIdC, addrsC);
|
|
auto nodeB = startDiscoPeer(peerIdC, addrsC);
|
|
|
|
std::string serviceId = "data-service";
|
|
std::string serviceData = "version=2;proto=test";
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->discoStartAdvertising(serviceId, serviceData, "").success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoRegisterInterest(serviceId).success);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
|
|
|
auto res = nodeB->discoLookup(serviceId, serviceData);
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
|
|
auto records = res.value;
|
|
LOGOS_ASSERT_FALSE(records.empty());
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_advertise_foreign_xpr) {
|
|
Libp2pModuleImpl nodeC(discoOptions());
|
|
LOGOS_ASSERT_TRUE(nodeC.start().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStart().success);
|
|
auto [peerIdC, addrsC] = getPeerInfoPair(nodeC);
|
|
|
|
auto nodeA = startDiscoPeer(peerIdC, addrsC);
|
|
auto nodeB = startDiscoPeer(peerIdC, addrsC);
|
|
|
|
// The consuming module's own switch: it mounts no service discovery, so only
|
|
// the record it hands over can carry it into the DHT.
|
|
Libp2pModuleImpl nodeS(Libp2pModuleOptions{ .mountServiceDiscovery = false });
|
|
LOGOS_ASSERT_TRUE(nodeS.start().success);
|
|
auto [peerIdS, addrsS] = getPeerInfoPair(nodeS);
|
|
|
|
std::string serviceId = "foreign-service";
|
|
std::string serviceData = "version=3";
|
|
std::map<std::string, std::vector<uint8_t>> services = {
|
|
{serviceId, std::vector<uint8_t>(serviceData.begin(), serviceData.end())},
|
|
};
|
|
|
|
auto xpr = nodeS.createXpr(addrsS, services, 0);
|
|
LOGOS_ASSERT_TRUE(xpr.success);
|
|
std::string advertisement = xpr.value.get<std::string>();
|
|
|
|
LOGOS_ASSERT_TRUE(
|
|
nodeA->discoStartAdvertising(serviceId, serviceData, advertisement).success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoRegisterInterest(serviceId).success);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
|
|
|
|
auto res = nodeB->discoLookup(serviceId, serviceData);
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
|
|
auto records = res.value;
|
|
LOGOS_ASSERT_FALSE(records.empty());
|
|
|
|
// The advertiser published someone else's record, so the discoverer reaches
|
|
// the service switch and not the advertiser.
|
|
LOGOS_ASSERT_EQ(records[0]["peerId"].get<std::string>(), peerIdS);
|
|
LOGOS_ASSERT_FALSE(records[0]["addrs"].empty());
|
|
LOGOS_ASSERT_EQ(records[0]["addrs"][0].get<std::string>(), addrsS[0]);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB->stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeC.stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeS.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_advertise_rejects_bad_xpr) {
|
|
Libp2pModuleImpl node(discoOptions());
|
|
LOGOS_ASSERT_TRUE(node.start().success);
|
|
|
|
std::string serviceId = "checked-service";
|
|
|
|
LOGOS_ASSERT_FALSE(node.discoStartAdvertising(serviceId, "", "not base64!").success);
|
|
|
|
auto [peerId, addrs] = getPeerInfoPair(node);
|
|
auto other = node.createXpr(addrs, {{"other-service", {}}}, 0);
|
|
LOGOS_ASSERT_TRUE(other.success);
|
|
LOGOS_ASSERT_FALSE(
|
|
node.discoStartAdvertising(serviceId, "", other.value.get<std::string>()).success);
|
|
|
|
auto signedXpr = node.createXpr(addrs, {{serviceId, {}}}, 0);
|
|
LOGOS_ASSERT_TRUE(signedXpr.success);
|
|
std::string rawBytes = base64Decode(signedXpr.value.get<std::string>());
|
|
rawBytes[rawBytes.size() / 2] ^= 0xff;
|
|
std::string tampered = base64Encode(
|
|
std::vector<uint8_t>(rawBytes.begin(), rawBytes.end()));
|
|
LOGOS_ASSERT_FALSE(node.discoStartAdvertising(serviceId, "", tampered).success);
|
|
|
|
LOGOS_ASSERT_TRUE(node.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_start_stop_advertising) {
|
|
Libp2pModuleImpl nodeA(discoOptions());
|
|
Libp2pModuleImpl nodeB(discoOptions());
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.start().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.start().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStart().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.discoStart().success);
|
|
|
|
auto [peerIdA, addrsA] = getPeerInfoPair(nodeA);
|
|
LOGOS_ASSERT_TRUE(nodeB.connectPeer(peerIdA, addrsA, 500).success);
|
|
|
|
std::string serviceId = "ephemeral-service";
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStartAdvertising(serviceId, "", "").success);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStopAdvertising(serviceId).success);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
auto res = nodeB.discoLookup(serviceId, "");
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
|
|
auto records = res.value;
|
|
LOGOS_ASSERT_TRUE(records.empty());
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(disco_random_lookup) {
|
|
Libp2pModuleImpl nodeA(discoOptions());
|
|
Libp2pModuleImpl nodeB(discoOptions());
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.start().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.start().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStart().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.discoStart().success);
|
|
|
|
auto [peerIdA, addrsA] = getPeerInfoPair(nodeA);
|
|
LOGOS_ASSERT_TRUE(nodeB.connectPeer(peerIdA, addrsA, 500).success);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
auto res = nodeA.discoRandomLookup();
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.discoStop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.discoStop().success);
|
|
|
|
LOGOS_ASSERT_TRUE(nodeA.stop().success);
|
|
LOGOS_ASSERT_TRUE(nodeB.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(create_xpr) {
|
|
Libp2pModuleImpl node(discoOptions());
|
|
LOGOS_ASSERT_TRUE(node.start().success);
|
|
|
|
auto [peerId, addrs] = getPeerInfoPair(node);
|
|
|
|
std::map<std::string, std::vector<uint8_t>> services = {
|
|
{"chat", {0x01, 0x02, 0x03}},
|
|
{"file-share", {}},
|
|
};
|
|
|
|
auto res = node.createXpr(addrs, services, 42);
|
|
LOGOS_ASSERT_TRUE(res.success);
|
|
LOGOS_ASSERT_FALSE(res.value.get<std::string>().empty());
|
|
|
|
// Only seqNo changes, so a differing payload isolates the seqNo branch.
|
|
auto resSeq = node.createXpr(addrs, services, 43);
|
|
LOGOS_ASSERT_TRUE(resSeq.success);
|
|
LOGOS_ASSERT_TRUE(res.value.get<std::string>() != resSeq.value.get<std::string>());
|
|
|
|
// Empty addrs falls back to listen addresses; seqNo 0 uses current time.
|
|
auto resDefaults = node.createXpr({}, {}, 0);
|
|
LOGOS_ASSERT_TRUE(resDefaults.success);
|
|
LOGOS_ASSERT_FALSE(resDefaults.value.get<std::string>().empty());
|
|
|
|
LOGOS_ASSERT_TRUE(node.stop().success);
|
|
}
|
|
|
|
LOGOS_TEST(decode_xpr) {
|
|
Libp2pModuleImpl node(discoOptions());
|
|
LOGOS_ASSERT_TRUE(node.start().success);
|
|
|
|
auto [peerId, addrs] = getPeerInfoPair(node);
|
|
|
|
// None of 0x80/0xfe/0xff is valid UTF-8 and 0x00 terminates a C string:
|
|
// together they catch a lossy UTF-8 round-trip and a truncating one alike.
|
|
const std::vector<uint8_t> binData{0x01, 0x00, 0x02, 0x80, 0xfe, 0xff, 0x03};
|
|
const std::string binDataStr(binData.begin(), binData.end());
|
|
std::map<std::string, std::vector<uint8_t>> services = {
|
|
{"chat", binData},
|
|
{"file-share", {}},
|
|
};
|
|
|
|
auto created = node.createXpr(addrs, services, 42);
|
|
LOGOS_ASSERT_TRUE(created.success);
|
|
|
|
// createXpr's base64 output feeds straight into decodeXpr, no manual decode.
|
|
std::string signedXpr = created.value.get<std::string>();
|
|
auto decoded = node.decodeXpr(signedXpr);
|
|
LOGOS_ASSERT_TRUE(decoded.success);
|
|
LOGOS_ASSERT_EQ(decoded.value["peerId"].get<std::string>(), peerId);
|
|
LOGOS_ASSERT_EQ(decoded.value["seqNo"].get<uint64_t>(), 42u);
|
|
LOGOS_ASSERT_EQ(decoded.value["services"].size(), services.size());
|
|
// std::map orders its keys, so "chat" precedes "file-share" on the wire.
|
|
LOGOS_ASSERT_EQ(decoded.value["services"][0]["id"].get<std::string>(), "chat");
|
|
// Every advertised byte survives — this is what `bstr` buys over the old
|
|
// std::string parameter, which mangled anything that was not valid UTF-8.
|
|
LOGOS_ASSERT_EQ(base64Decode(decoded.value["services"][0]["data"].get<std::string>()),
|
|
binDataStr);
|
|
LOGOS_ASSERT_EQ(decoded.value["services"][1]["id"].get<std::string>(), "file-share");
|
|
LOGOS_ASSERT_TRUE(decoded.value["services"][1]["data"].get<std::string>().empty());
|
|
|
|
// A flipped byte must fail signature verification, not silently decode.
|
|
std::string rawBytes = base64Decode(signedXpr);
|
|
rawBytes[rawBytes.size() / 2] ^= 0xff;
|
|
std::string tampered = base64Encode(
|
|
std::vector<uint8_t>(rawBytes.begin(), rawBytes.end()));
|
|
LOGOS_ASSERT_FALSE(node.decodeXpr(tampered).success);
|
|
|
|
LOGOS_ASSERT_FALSE(node.decodeXpr("").success);
|
|
|
|
LOGOS_ASSERT_TRUE(node.stop().success);
|
|
}
|