Files
MeshCore/test/test_companion_tx_raw_alias.py
T
mikecarper 4b81a9d5e0 Persist unchanged Companion channel route SETs before OK
Require a real raw-table save for same-value channel SETs. This repairs the false acknowledgement after successful constructor recovery of an older backup followed by a failed candidate write.

Keep resolved alias keys, raw AUTO policy and runtime generation untouched. Preserve GET and unchanged contact behavior, changed-policy validation and indeterminate-publication quarantine.

Cover real stale-backup creation, failure/retry, reboot and exact old-skip controls. Add actual alias/BaseChatMesh and raw app-key regression tests on all four backends. Verified 117 related host tests, 16 final caller rechecks, four firmware qualifications and linked nRF52 crypto checks.
2026-10-16 11:34:51 -07:00

177 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Keep actual channel aliases raw across durable same-value TX route SETs."""
from pathlib import Path
import hashlib
import shutil
import subprocess
import tempfile
import unittest
from test_companion_channel_publication import publication_fixture
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
def replace_once(source, before, after):
assert source.count(before) == 1, before
return source.replace(before, after, 1)
def raw_alias_fixture(work):
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="ascii")
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="ascii")
base = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text(encoding="ascii")
# The alias/key helpers execute unchanged. Supply independent SHA-256
# vectors only at their crypto boundary, including cached channel hashes.
vectors = [b"#r2-emergency", b"#emergency", bytes(16)]
vectors += [hashlib.sha256(name).digest()[:16] for name in vectors[:2]]
sha = "static void sha256(uint8_t* out, size_t size, const uint8_t* input, int length) {\n"
for raw in vectors:
data = ",".join(str(value) for value in raw)
digest = ",".join(str(value) for value in hashlib.sha256(raw).digest())
sha += (" { static const uint8_t in[] = {" + data + "};\n"
" static const uint8_t hash[] = {" + digest + "};\n"
" if (length == int(sizeof(in)) && !memcmp(in,input,sizeof(in))) "
"{ assert(size <= sizeof(hash)); memcpy(out,hash,size); return; } }\n")
sha += ' assert(!"input outside independent SHA256 oracle");\n}\n'
generated = publication_fixture(work, False)
code = generated.read_text(encoding="ascii")
code = code[:code.index("int main() {")]
code = replace_once(code, "struct Utils {", "struct Utils {\n" + sha)
# Replace the publication fixture's simplified channel boundary with the
# real raw accessors, alias resolution and generation-changing setter.
for signature, declaration in (
(" bool getChannel(int i, ChannelDetails& c)", " bool getChannel(int, ChannelDetails&);"),
(" bool setChannel(int i, const ChannelDetails& c)", " bool setChannel(int, const ChannelDetails&);"),
(" bool isAliasRadio2Defined() const", " bool isAliasRadio2Defined() const;"),
(" bool resolveChannelAlias(ChannelDetails&) const", " bool resolveChannelAlias(ChannelDetails&) const;"),
):
code = replace_once(code, method(code, signature), declaration)
for signature, production in (
(" bool getChannelForSave(uint8_t i,ChannelDetails& c)", " bool getChannelForSave(uint8_t channel_idx,"),
(" bool onChannelLoaded(uint8_t i,const ChannelDetails& c)", " bool onChannelLoaded(uint8_t channel_idx,"),
):
code = replace_once(code, method(code, signature), method(header, production))
code = replace_once(code, " ChannelDetails channels[MAX_GROUP_CHANNELS];\n", "")
code = replace_once(code, "class MyMesh : public DataStoreHost {", """
class BaseChatMesh {
public:
ChannelDetails channels[MAX_GROUP_CHANNELS];
bool getChannel(int, ChannelDetails&);
bool setChannel(int, const ChannelDetails&);
};
class MyMesh : public BaseChatMesh, public DataStoreHost {""")
code = replace_once(code, " bool writable=true, writes_ok=true; unsigned writes=0;", """
struct { bool enabled = false; } _ethernet_settings;
bool writable=true, writes_ok=true; unsigned writes=0;""")
assert "unsigned channel_sets=0, _ethernet_channel_generation=0;" in code
code += "\n" + "\n".join(method(base, signature) for signature in (
"bool BaseChatMesh::getChannel(", "bool BaseChatMesh::setChannel("))
code += "\n" + "\n".join(method(mesh, signature) for signature in (
"bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(",
"bool MyMesh::getChannel(", "bool MyMesh::setChannel("))
code += r'''
int main() {
MyMesh node;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
node.store.fs.rename_replaces = true;
#endif
strcpy(node.channels[0].name, "#r2-emergency");
mesh::Utils::sha256(node.channels[0].channel.secret, 16,
reinterpret_cast<const uint8_t*>(node.channels[0].name), strlen(node.channels[0].name));
node.channels[0].channel.tx_radio = mesh::RADIO_TX_AUTO;
node.radio.config.secondary.mode = mesh::RadioProfileMode::RxTx;
node.radio.config.cross = mesh::RadioCrossMode::Off;
strcpy(node.contacts[1].name, "Healthy contact");
node.contacts[1].tx_radio = mesh::RADIO_TX_PRIMARY;
assert(node.store.saveChannels(&node));
const ChannelDetails raw = node.channels[0];
const auto original = node.store.fs.files.at("/channels2");
uint8_t canonical_key[16];
mesh::Utils::sha256(canonical_key, sizeof(canonical_key),
reinterpret_cast<const uint8_t*>("#emergency"), strlen("#emergency"));
ChannelDetails effective;
assert(node.getChannel(0, effective));
assert(effective.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
assert(memcmp(effective.channel.secret, raw.channel.secret, 16));
assert(!memcmp(effective.channel.secret, canonical_key, sizeof(canonical_key)));
char reply[160] = {};
const auto generation = node._ethernet_channel_generation;
auto writes = node.writes;
node.store.fs.fail_write = true;
assert(node.handleTxRoutingCommand("set tx.channel 0 radio2", reply, sizeof(reply)));
assert(strstr(reply, "not saved") && node.writes == writes + 1);
assert(!memcmp(&node.channels[0], &raw, sizeof(raw)));
assert(node._ethernet_channel_generation == generation);
assert(node.store.fs.files.at("/channels2") == original);
assert(!node.store.hasIncompleteChannelLoad());
node.store.fs.fail_write = false;
writes = node.writes;
assert(node.handleTxRoutingCommand("set tx.channel 0 radio2", reply, sizeof(reply)));
assert(!strncmp(reply, "OK", 2) && node.writes == writes + 1);
assert(!memcmp(&node.channels[0], &raw, sizeof(raw)));
assert(node._ethernet_channel_generation == generation);
assert(node.store.fs.files.at("/channels2") == original);
assert(original[0] == mesh::encodeRadioTxPolicy(mesh::RADIO_TX_AUTO));
assert(!memcmp(original.data() + 36, raw.channel.secret, 32));
writes = node.writes;
assert(node.handleTxRoutingCommand("get tx.channel 0", reply, sizeof(reply)));
assert(node.handleTxRoutingCommand("set tx.user \"Healthy contact\" radio", reply, sizeof(reply)));
assert(!strncmp(reply, "OK", 2) && node.writes == writes);
assert(node.handleTxRoutingCommand("set tx.channel 0 radio", reply, sizeof(reply)));
assert(strstr(reply, "prefix controls routing") && node.writes == writes);
MyMesh reboot;
reboot.store.fs.files = node.store.fs.files;
reboot.radio.config.secondary = node.radio.config.secondary;
reboot.store.loadChannels(&reboot);
assert(reboot.channels[0].channel.tx_radio == mesh::RADIO_TX_AUTO);
assert(!memcmp(reboot.channels[0].channel.secret, raw.channel.secret, 32));
assert(reboot.getChannel(0, effective));
assert(effective.channel.tx_radio == mesh::RADIO_TX_SECONDARY);
assert(!memcmp(effective.channel.secret, canonical_key, sizeof(canonical_key)));
puts("PASS: actual channel SET retry, raw AUTO/app PSK and effective SECONDARY/wire key");
}
'''
generated.write_text(code, encoding="ascii")
return generated
class CompanionTxRawAliasTest(unittest.TestCase):
def run_platform(self, platform):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a C++ compiler is required")
with tempfile.TemporaryDirectory(prefix="channel-samevalue-real-alias-") as directory:
work = Path(directory)
generated = raw_alias_fixture(work)
binary = work / "test"
compiled = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-D" + platform + "=1",
"-DMESH_CONTACT_CACHE=0", "-DMESH_ENABLE_MESHTEXT_CHAT=0", "-DMESH_ENABLE_ETHERNET_TUNNEL=1",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-I", str(work), "-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"), "-I", str(ROOT / "src/helpers"),
str(generated), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
self.assertIn("PASS: actual channel SET retry", tested.stdout)
def test_nrf52_raw_alias_failure_and_retry(self):
self.run_platform("NRF52_PLATFORM")
def test_stm32_raw_alias_failure_and_retry(self):
self.run_platform("STM32_PLATFORM")
def test_esp32_raw_alias_failure_and_retry(self):
self.run_platform("ESP32_PLATFORM")
def test_rp2040_raw_alias_failure_and_retry(self):
self.run_platform("RP2040_PLATFORM")
if __name__ == "__main__":
unittest.main()