Files
MeshCore/test/test_room_catchup_transport.py
T

207 lines
10 KiB
Python

#!/usr/bin/env python3
"""Execute actual Mesh cancellation and production room delivery identities."""
import hashlib
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
#include <openssl/sha.h>
#include <MeshCore.h>
#include <helpers/RoomClientPathCommand.h>
@CONSTANTS@
namespace mesh {
struct Packet {};
struct Mesh {
@DIRECT_ENTRY@;
@FLOOD_ENTRY@;
DirectRetryEntry _direct_retries[TOTAL_DIRECT_RETRY_SLOTS]{};
FloodRetryEntry _flood_retries[TOTAL_FLOOD_RETRY_SLOTS]{};
std::vector<int> direct_retired, flood_retired;
void retireDirectRetrySlot(int index) {
assert(_direct_retries[index].active); direct_retired.push_back(index);
memset(&_direct_retries[index], 0, sizeof(_direct_retries[index]));
}
void retireFloodRetrySlot(int index) {
assert(_flood_retries[index].active); flood_retired.push_back(index);
memset(&_flood_retries[index], 0, sizeof(_flood_retries[index]));
}
bool cancelActiveMessageRetries(const uint8_t*, uint32_t);
};
@CANCEL@
}
static void hash(uint8_t* output, size_t size, const uint8_t* input, size_t length) {
uint8_t digest[SHA256_DIGEST_LENGTH];
assert(size <= sizeof(digest));
SHA256(input, length, digest); memcpy(output, digest, size);
}
static void makeKey(uint8_t* output, const uint8_t* room, const uint8_t* recipient, bool topic) {
mesh::roomDeliveryRetryKey(output, MAX_HASH_SIZE, room, recipient, topic, hash);
}
static void identityKeys() {
uint8_t room[32], recipient[32]; memset(room, 1, 32); memset(recipient, 2, 32);
uint8_t primary[MAX_HASH_SIZE], topic[MAX_HASH_SIZE], other_room[MAX_HASH_SIZE], other_user[MAX_HASH_SIZE];
makeKey(primary, room, recipient, false); makeKey(topic, room, recipient, true);
const uint8_t expected[] = {@EXPECTED_KEY@};
assert(memcmp(primary, expected, sizeof(expected)) == 0);
room[31] = 3; makeKey(other_room, room, recipient, false);
room[31] = 1; recipient[31] = 3; makeKey(other_user, room, recipient, false);
assert(memcmp(primary, topic, MAX_HASH_SIZE) != 0);
assert(memcmp(primary, other_room, MAX_HASH_SIZE) != 0);
assert(memcmp(primary, other_user, MAX_HASH_SIZE) != 0);
recipient[31] = 2; uint8_t repeated[MAX_HASH_SIZE]; makeKey(repeated, room, recipient, false);
assert(memcmp(primary, repeated, MAX_HASH_SIZE) == 0);
puts("full room and recipient identities and topic namespace match SHA256 reference passed");
}
template <typename Entry>
static void seed(Entry& entry, const uint8_t* key, uint32_t timestamp, bool recognized = true) {
entry.active = true; entry.message_timestamp = timestamp;
entry.has_message_replacement_key = recognized;
memcpy(entry.message_replacement_key, key, MAX_HASH_SIZE);
}
static void cancellationCases() {
uint8_t room[32], user[32]; memset(room, 1, 32); memset(user, 2, 32);
uint8_t primary[MAX_HASH_SIZE], topic[MAX_HASH_SIZE], other[MAX_HASH_SIZE];
makeKey(primary, room, user, false); makeKey(topic, room, user, true);
user[31] ^= 1; makeKey(other, room, user, false);
mesh::Mesh mesh;
for (int profile = 0; profile < 2; ++profile) {
const int d = profile * MAX_DIRECT_RETRY_SLOTS, f = profile * MAX_FLOOD_RETRY_SLOTS;
seed(mesh._direct_retries[d], primary, 500); seed(mesh._flood_retries[f], primary, 500);
seed(mesh._direct_retries[d + 1], primary, 501); seed(mesh._flood_retries[f + 1], primary, 501);
seed(mesh._direct_retries[d + 2], topic, 500); seed(mesh._flood_retries[f + 2], topic, 500);
seed(mesh._direct_retries[d + 3], other, 500); seed(mesh._flood_retries[f + 3], other, 500);
seed(mesh._direct_retries[d + 4], primary, 500, false); seed(mesh._flood_retries[f + 4], primary, 500, false);
mesh._direct_retries[d].queued = profile == 0;
mesh._flood_retries[f].waiting_final_echo = profile == 1;
}
assert(!mesh.cancelActiveMessageRetries(nullptr, 500));
assert(!mesh.cancelActiveMessageRetries(primary, 499));
const auto before = mesh;
assert(mesh.cancelActiveMessageRetries(primary, 500));
assert(mesh.direct_retired.size() == 2 && mesh.flood_retired.size() == 2);
for (int profile = 0; profile < 2; ++profile) {
const int d = profile * MAX_DIRECT_RETRY_SLOTS, f = profile * MAX_FLOOD_RETRY_SLOTS;
assert(!mesh._direct_retries[d].active && !mesh._flood_retries[f].active);
for (int offset = 1; offset < MAX_DIRECT_RETRY_SLOTS; ++offset) {
assert(memcmp(&mesh._direct_retries[d + offset], &before._direct_retries[d + offset],
sizeof(mesh._direct_retries[0])) == 0);
}
for (int offset = 1; offset < MAX_FLOOD_RETRY_SLOTS; ++offset) {
assert(memcmp(&mesh._flood_retries[f + offset], &before._flood_retries[f + offset],
sizeof(mesh._flood_retries[0])) == 0);
}
}
assert(!mesh.cancelActiveMessageRetries(primary, 500));
// Retirement clears the slot's key. An input alias must survive all matches.
mesh::Mesh alias;
seed(alias._direct_retries[0], primary, 500);
seed(alias._direct_retries[MAX_DIRECT_RETRY_SLOTS], primary, 500);
seed(alias._flood_retries[0], primary, 500);
seed(alias._flood_retries[MAX_FLOOD_RETRY_SLOTS], primary, 500);
assert(alias.cancelActiveMessageRetries(alias._direct_retries[0].message_replacement_key, 500));
assert(alias.direct_retired.size() == 2 && alias.flood_retired.size() == 2);
puts("actual cancellation retires one delivery across profiles and preserves every unrelated retry passed");
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "keys")) identityKeys();
else if (!strcmp(argv[1], "cancel")) cancellationCases();
else assert(false);
}
'''
def production_source(cancel_override=None):
source = (ROOT / "src/Mesh.cpp").read_text()
header = (ROOT / "src/Mesh.h").read_text()
constants = []
for name in ("MAX_DIRECT_RETRY_SLOTS", "MAX_FLOOD_RETRY_SLOTS", "TOTAL_DIRECT_RETRY_SLOTS", "TOTAL_FLOOD_RETRY_SLOTS"):
found = re.search(r"^\s*#define\s+" + name + r"\s+.*$", header, re.MULTILINE)
if not found:
raise AssertionError("production retry capacity not found: " + name)
constants.append(found[0])
digest = hashlib.sha256(b"room\x00" + bytes([1]) * 32 + bytes([2]) * 32).digest()[:8]
replacements = {
"@CONSTANTS@": "\n".join(constants),
"@DIRECT_ENTRY@": extract_braced(header, "struct DirectRetryEntry {"),
"@FLOOD_ENTRY@": extract_braced(header, "struct FloodRetryEntry {"),
"@CANCEL@": cancel_override or extract_braced(source, "bool Mesh::cancelActiveMessageRetries("),
"@EXPECTED_KEY@": ",".join("0x%02X" % byte for byte in digest),
}
generated = HARNESS
for marker, value in replacements.items():
generated = generated.replace(marker, value)
return generated
class RoomCatchUpTransportTests(unittest.TestCase):
@classmethod
def build(cls, name, source):
work = Path(cls.directory.name)
path = work / (name + ".cpp"); path.write_text(source, encoding="ascii")
binary = work / name
command = [cls.compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-I" + str(ROOT / "src"), str(path), "-lcrypto", "-o", str(binary)]
if sys.platform.startswith("linux"):
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
checked = subprocess.run(command, capture_output=True, text=True, timeout=60)
if checked.returncode:
raise AssertionError(checked.stdout + checked.stderr)
return binary
@classmethod
def setUpClass(cls):
cls.compiler = shutil.which("g++") or shutil.which("clang++")
if not cls.compiler:
raise AssertionError("a host C++17 compiler is required")
cls.directory = tempfile.TemporaryDirectory(prefix="room-catchup-transport-")
cls.addClassCleanup(cls.directory.cleanup)
cls.binary = cls.build("production", production_source())
def run_case(self, name, expected):
checked = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=15)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(expected, checked.stdout)
def test_actual_domain_key_covers_full_room_recipient_and_topic_namespace(self):
self.run_case("keys", "full room and recipient identities and topic namespace match SHA256 reference passed")
def test_actual_mesh_cancellation_key_timestamp_kind_and_both_profiles(self):
self.run_case("cancel", "actual cancellation retires one delivery across profiles and preserves every unrelated retry passed")
def test_negative_controls_detect_broad_timestamp_and_key_cancellation(self):
source = (ROOT / "src/Mesh.cpp").read_text()
cancel = extract_braced(source, "bool Mesh::cancelActiveMessageRetries(")
for kind, before in (("timestamp", ".message_timestamp == message_timestamp"),
("recognition", ".has_message_replacement_key")):
with self.subTest(control=kind):
self.assertEqual(cancel.count(before), 2)
changed = cancel.replace(before, before + " || true")
# Preserve expression grouping while deliberately relaxing just this gate.
for table in ("_direct_retries[i]", "_flood_retries[i]"):
changed = changed.replace(table + before + " || true", "(" + table + before + " || true)")
binary = self.build("negative-" + kind, production_source(changed))
checked = subprocess.run([str(binary), "cancel"], capture_output=True, text=True, timeout=15)
self.assertNotEqual(checked.returncode, 0, "negative control missed " + kind)
self.assertIn("Assertion", checked.stderr, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()