mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-31 01:52:46 +00:00
317 lines
14 KiB
Python
317 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the C3 compact-held-DM policy through the real Companion methods."""
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def block(text, signature):
|
|
start = text.index(signature)
|
|
end = text.index("{", start) + 1
|
|
depth = 1
|
|
while depth:
|
|
depth += (text[end] == "{") - (text[end] == "}")
|
|
end += 1
|
|
return text[start:end]
|
|
|
|
|
|
class CompactHeldDmTest(unittest.TestCase):
|
|
def test_independent_capacity_fifo_promotion_metadata_ack_and_retry_dedup(self):
|
|
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
|
|
methods = "\n".join(block(source, signature) for signature in (
|
|
"bool MyMesh::Frame::isChannelMsg() const",
|
|
"int MyMesh::getOfflineQueueCapacity() const",
|
|
"MyMesh::Frame& MyMesh::offlineQueueFrameAt(int logical_index)",
|
|
"bool MyMesh::addToOfflineQueue(const uint8_t frame[], int len)",
|
|
"int MyMesh::peekOfflineQueue(uint8_t frame[])",
|
|
"void MyMesh::popOfflineQueue()",
|
|
"int MyMesh::getFromOfflineQueue(uint8_t frame[])",
|
|
"void MyMesh::forgetVerifiedPendingSender(const uint8_t* pub_key)",
|
|
"void MyMesh::makeOneKeyDMId(uint8_t id[ONE_KEY_DM_ID_SIZE], uint32_t timestamp,",
|
|
"bool MyMesh::wasDeliveredOneKeyDM(const uint8_t* pub_key,",
|
|
"void MyMesh::rememberDeliveredOneKeyDM(const uint8_t* pub_key,",
|
|
"bool MyMesh::onAddressedTextPacket(mesh::Packet* packet, uint8_t src_hash,",
|
|
"void MyMesh::releaseHeldOneKeyDMs()",
|
|
"void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,",
|
|
))
|
|
harness = r'''
|
|
#include <helpers/BorrowableFrameBuffer.h>
|
|
#include <openssl/sha.h>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
#define ONE_KEY_DM_COMPACT_HELD 1
|
|
#define ONE_KEY_DM_SHARED_OFFLINE_QUEUE 0
|
|
#define OTA_SHARED_COMPANION_QUEUE 1
|
|
#define OFFLINE_QUEUE_SIZE 224
|
|
#define MAX_FRAME_SIZE 176
|
|
#define MAX_PACKET_PAYLOAD 184
|
|
#define MAX_TEXT_LEN 160
|
|
#define PUB_KEY_SIZE 32
|
|
#define TXT_TYPE_PLAIN 0
|
|
#define ROUTE_TYPE_DIRECT 2
|
|
#define ROUTE_TYPE_FLOOD 1
|
|
#define MESH_ENABLE_ONE_KEY_DM 1
|
|
#define MESH_ENABLE_LOST_REPLY 0
|
|
#define RESP_CODE_CHANNEL_MSG_RECV 0x8c
|
|
#define RESP_CODE_CHANNEL_MSG_RECV_V3 0x90
|
|
#define RESP_CODE_CHANNEL_DATA_RECV 0x8d
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
namespace mesh {
|
|
struct Packet {
|
|
uint8_t header = ROUTE_TYPE_FLOOD;
|
|
uint8_t path_len = 0;
|
|
int8_t _snr = 0;
|
|
bool isRouteFlood() const { return header == ROUTE_TYPE_FLOOD; }
|
|
};
|
|
struct Utils {
|
|
static bool authentic;
|
|
static int MACThenDecrypt(const uint8_t*, uint8_t* out,
|
|
const uint8_t* in, size_t len) {
|
|
if (!authentic) return 0;
|
|
memcpy(out, in, len);
|
|
return static_cast<int>(len);
|
|
}
|
|
static void sha256(uint8_t* out, size_t size, const uint8_t* a, size_t a_size,
|
|
const uint8_t* b, size_t b_size) {
|
|
std::vector<uint8_t> input(a, a + a_size);
|
|
input.insert(input.end(), b, b + b_size);
|
|
uint8_t digest[SHA256_DIGEST_LENGTH];
|
|
SHA256(input.data(), input.size(), digest);
|
|
memcpy(out, digest, size);
|
|
}
|
|
};
|
|
bool Utils::authentic = true;
|
|
namespace ota { struct OtaContext { uint8_t bytes[11284]; }; }
|
|
}
|
|
struct ContactInfo { struct { uint8_t pub_key[PUB_KEY_SIZE] = {}; } id; };
|
|
struct SelfIdentity {
|
|
void calcSharedSecret(uint8_t* secret, const uint8_t*) { memset(secret, 0, PUB_KEY_SIZE); }
|
|
};
|
|
struct Delivery {
|
|
uint32_t timestamp;
|
|
std::string text;
|
|
uint8_t route, path;
|
|
int8_t snr;
|
|
};
|
|
class MyMesh {
|
|
public:
|
|
static constexpr uint8_t MAX_HELD_ONE_KEY_DMS = 15;
|
|
static constexpr uint8_t ONE_KEY_DM_ID_SIZE = 8;
|
|
struct Frame {
|
|
uint8_t len = 0, buf[MAX_FRAME_SIZE] = {};
|
|
bool isChannelMsg() const;
|
|
};
|
|
@HELD@;
|
|
@DELIVERED@;
|
|
static_assert(sizeof(HeldOneKeyDM) == 204, "15 slots must save 1740 bytes");
|
|
mesh::BorrowableFrameBuffer<Frame, 224, 128, mesh::ota::OtaContext> offline_queue;
|
|
int offline_queue_len = 0, offline_queue_head = 0;
|
|
HeldOneKeyDM held_dms[MAX_HELD_ONE_KEY_DMS] = {};
|
|
uint8_t held_dm_count = 0;
|
|
uint8_t verified_pending_keys[MAX_HELD_ONE_KEY_DMS][PUB_KEY_SIZE] = {};
|
|
uint8_t verified_pending_count = 1;
|
|
DeliveredOneKeyDM delivered_dms[MAX_HELD_ONE_KEY_DMS] = {};
|
|
uint8_t delivered_dm_count = 0, delivered_dm_next = 0;
|
|
std::vector<Delivery> deliveries;
|
|
std::vector<std::array<uint8_t, 4>> acks;
|
|
bool accepted = false;
|
|
unsigned writes = 0, active = 0;
|
|
ContactInfo contact;
|
|
SelfIdentity self_id;
|
|
MyMesh() {
|
|
for (unsigned i = 0; i < PUB_KEY_SIZE; ++i)
|
|
contact.id.pub_key[i] = verified_pending_keys[0][i] = 0x42 + i;
|
|
}
|
|
int getOfflineQueueCapacity() const;
|
|
Frame& offlineQueueFrameAt(int index);
|
|
bool addToOfflineQueue(const uint8_t frame[], int len);
|
|
int peekOfflineQueue(uint8_t frame[]);
|
|
void popOfflineQueue();
|
|
int getFromOfflineQueue(uint8_t frame[]);
|
|
bool onAddressedTextPacket(mesh::Packet*, uint8_t, const uint8_t*, size_t);
|
|
void releaseHeldOneKeyDMs();
|
|
void forgetVerifiedPendingSender(const uint8_t*);
|
|
static void makeOneKeyDMId(uint8_t[ONE_KEY_DM_ID_SIZE], uint32_t, const char*);
|
|
bool wasDeliveredOneKeyDM(const uint8_t*, const uint8_t[ONE_KEY_DM_ID_SIZE]) const;
|
|
void rememberDeliveredOneKeyDM(const uint8_t*, const uint8_t[ONE_KEY_DM_ID_SIZE]);
|
|
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*);
|
|
ContactInfo* lookupPersistentContactByPubKey(const uint8_t* key, size_t) {
|
|
return accepted && !memcmp(contact.id.pub_key, key, PUB_KEY_SIZE) ? &contact : nullptr;
|
|
}
|
|
void markConnectionActive(const ContactInfo&) { ++active; }
|
|
void scheduleContactWrite(const ContactInfo&) { ++writes; }
|
|
bool queueMessage(const ContactInfo&, uint8_t, mesh::Packet* packet,
|
|
uint32_t timestamp, const uint8_t*, int, const char* text) {
|
|
uint8_t frame[MAX_FRAME_SIZE] = {1}; // private, never evicted as a channel
|
|
memcpy(frame + 1, ×tamp, 4);
|
|
memcpy(frame + 5, text, strlen(text));
|
|
if (!addToOfflineQueue(frame, 5 + strlen(text))) return false;
|
|
deliveries.push_back({timestamp, text, packet->header, packet->path_len, packet->_snr});
|
|
return true;
|
|
}
|
|
void sendAckTo(const ContactInfo&, const uint8_t* hash, uint8_t length) {
|
|
assert(length == 4);
|
|
std::array<uint8_t, 4> ack;
|
|
memcpy(ack.data(), hash, 4);
|
|
acks.push_back(ack);
|
|
}
|
|
void fill() {
|
|
const uint8_t frame = 1;
|
|
while (offline_queue_len < getOfflineQueueCapacity()) assert(addToOfflineQueue(&frame, 1));
|
|
}
|
|
void drain() { uint8_t frame[MAX_FRAME_SIZE]; while (getFromOfflineQueue(frame)) {} }
|
|
};
|
|
@METHODS@
|
|
struct Message {
|
|
uint32_t timestamp;
|
|
std::string text;
|
|
mesh::Packet packet;
|
|
std::vector<uint8_t> plain;
|
|
std::array<uint8_t, 4> ack;
|
|
Message(unsigned id, size_t length = 0) : timestamp(id) {
|
|
text = length ? std::string(length, 'x') : "message-" + std::to_string(id);
|
|
if (length) { text[length - 2] = char(0xC3); text[length - 1] = char(0xA9); }
|
|
plain.resize(5 + text.size());
|
|
memcpy(plain.data(), ×tamp, 4);
|
|
plain[4] = id % 4; // real attempt bits must survive in the exact ACK
|
|
memcpy(plain.data() + 5, text.data(), text.size());
|
|
packet.header = id % 2 ? ROUTE_TYPE_DIRECT : ROUTE_TYPE_FLOOD;
|
|
packet.path_len = id % 2 ? 21 : 0x82;
|
|
packet._snr = id % 2 ? -91 : 51;
|
|
// Independently concatenate and hash exactly the original plaintext/key.
|
|
std::vector<uint8_t> input = plain;
|
|
for (unsigned i = 0; i < PUB_KEY_SIZE; ++i) input.push_back(0x42 + i);
|
|
uint8_t digest[SHA256_DIGEST_LENGTH];
|
|
SHA256(input.data(), input.size(), digest);
|
|
memcpy(ack.data(), digest, 4);
|
|
}
|
|
bool hold(MyMesh& receiver) {
|
|
return receiver.onAddressedTextPacket(&packet, 0x42, plain.data(), plain.size());
|
|
}
|
|
};
|
|
int main() {
|
|
{
|
|
MyMesh receiver;
|
|
std::vector<Message> messages;
|
|
for (unsigned i = 1; i <= 16; ++i) {
|
|
messages.emplace_back(i, i == 16 ? MAX_TEXT_LEN : 0);
|
|
assert(messages.back().hold(receiver));
|
|
}
|
|
assert(receiver.held_dm_count == 15 && receiver.held_dms[0].timestamp == 2);
|
|
assert(receiver.held_dms[14].text[MAX_TEXT_LEN] == 0);
|
|
auto original_ack = receiver.held_dms[14].ack_hash[0];
|
|
messages.back().plain[4] ^= 1; // same text/timestamp, different attempt
|
|
assert(messages.back().hold(receiver));
|
|
assert(receiver.held_dm_count == 15 && receiver.held_dms[14].ack_hash[0] == original_ack);
|
|
// Pending storage stays independent even with every normal slot occupied.
|
|
receiver.fill();
|
|
assert(receiver.offline_queue_len == 224 && receiver.held_dm_count == 15);
|
|
receiver.accepted = true;
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.verified_pending_count == 0 && receiver.held_dm_count == 15);
|
|
assert(receiver.deliveries.empty() && receiver.acks.empty());
|
|
receiver.popOfflineQueue();
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.held_dm_count == 14 && receiver.offline_queue_len == 224);
|
|
assert(receiver.deliveries.size() == 1 && receiver.deliveries[0].timestamp == 2);
|
|
receiver.drain();
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.held_dm_count == 0 && receiver.deliveries.size() == 15);
|
|
assert(receiver.acks.size() == 15 && receiver.offline_queue_len == 14);
|
|
for (unsigned i = 0; i < 15; ++i) {
|
|
const Delivery& delivery = receiver.deliveries[i];
|
|
const Message& expected = messages[i + 1];
|
|
assert(delivery.timestamp == expected.timestamp && delivery.text == expected.text);
|
|
assert(delivery.route == expected.packet.header && delivery.snr == expected.packet._snr);
|
|
assert(delivery.path == (expected.packet.isRouteFlood() ? expected.packet.path_len : 0));
|
|
assert(receiver.acks[i] == expected.ack);
|
|
}
|
|
}
|
|
{
|
|
MyMesh receiver;
|
|
Message message(7, MAX_TEXT_LEN);
|
|
assert(message.hold(receiver));
|
|
receiver.fill();
|
|
receiver.accepted = true;
|
|
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
|
|
assert(receiver.deliveries.empty() && receiver.held_dm_count == 1);
|
|
receiver.popOfflineQueue();
|
|
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
|
|
assert(receiver.deliveries.size() == 1 && receiver.offline_queue_len == 224);
|
|
receiver.drain();
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.held_dm_count == 0 && receiver.deliveries.size() == 1);
|
|
assert(receiver.acks.size() == 1 && receiver.acks[0] == message.ack);
|
|
receiver.onMessageRecv(receiver.contact, &message.packet, message.timestamp, message.text.c_str());
|
|
assert(receiver.deliveries.size() == 1); // already delivered retry stays deduplicated
|
|
}
|
|
{
|
|
MyMesh receiver;
|
|
Message message(33, MAX_TEXT_LEN);
|
|
mesh::Utils::authentic = false;
|
|
assert(!message.hold(receiver) && receiver.held_dm_count == 0);
|
|
mesh::Utils::authentic = true;
|
|
Message oversized(34, MAX_TEXT_LEN + 1);
|
|
assert(!oversized.hold(receiver) && receiver.held_dm_count == 0);
|
|
assert(!receiver.onAddressedTextPacket(&message.packet, 0x99,
|
|
message.plain.data(), message.plain.size()));
|
|
message.plain[4] = 4; // non-plain text must not enter the compact FIFO
|
|
assert(!message.hold(receiver) && receiver.held_dm_count == 0);
|
|
message.plain[4] = 1;
|
|
assert(message.hold(receiver));
|
|
receiver.fill();
|
|
for (unsigned i = 0; i < 96; ++i) receiver.popOfflineQueue();
|
|
auto* workspace = receiver.offline_queue.acquire(receiver.offline_queue_len,
|
|
receiver.offline_queue_head);
|
|
assert(workspace && receiver.getOfflineQueueCapacity() == 128);
|
|
memset(workspace->bytes, 0xCC, sizeof workspace->bytes);
|
|
assert(!strcmp(receiver.held_dms[0].text, message.text.c_str()));
|
|
receiver.offline_queue.release(receiver.offline_queue_head);
|
|
assert(receiver.getOfflineQueueCapacity() == 224);
|
|
receiver.drain();
|
|
receiver.accepted = true;
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.deliveries.size() == 1 && receiver.deliveries[0].text == message.text);
|
|
assert(receiver.acks.size() == 1 && receiver.acks[0] == message.ack);
|
|
}
|
|
{
|
|
MyMesh receiver;
|
|
Message first(50), second(50, 24);
|
|
assert(first.hold(receiver) && second.hold(receiver));
|
|
assert(receiver.held_dm_count == 2); // equal timestamps do not merge different text
|
|
assert(first.hold(receiver) && receiver.held_dm_count == 2);
|
|
receiver.accepted = true;
|
|
receiver.releaseHeldOneKeyDMs();
|
|
assert(receiver.deliveries.size() == 2 && receiver.acks.size() == 2);
|
|
assert(receiver.deliveries[0].text == first.text && receiver.deliveries[1].text == second.text);
|
|
}
|
|
}
|
|
'''
|
|
harness = harness.replace("@HELD@", block(header, "struct HeldOneKeyDM {"))
|
|
harness = harness.replace("@DELIVERED@", block(header, "struct DeliveredOneKeyDM {"))
|
|
harness = harness.replace("@METHODS@", methods)
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-compact-dm-") as temp:
|
|
binary = Path(temp) / "compact"
|
|
built = subprocess.run([
|
|
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-fsanitize=address,undefined", "-g", "-I", str(ROOT / "src"),
|
|
"-x", "c++", "-", "-lcrypto", "-o", str(binary),
|
|
], input=harness, text=True, capture_output=True)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
result = subprocess.run([str(binary)], text=True, capture_output=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|