Preserve ACK ownership across contact relocation and retirement

This commit is contained in:
mikecarper committed 2026-10-03 21:10:49 -07:00
1 parent d29cba046b
commit ea938e8e84
15 files changed
+907 -50

No files matched your search

+1
View File
@@ -131,6 +131,7 @@ jobs:
python3 -B test/test_companion_primary_radio_persistence.py -v
python3 -B test/test_companion_response_bounds.py -v
python3 -B test/test_companion_ack_delivery.py -v
python3 -B test/test_companion_contact_lifetime.py -v
python3 -B test/test_companion_identity_cli.py -v
python3 -B test/test_companion_uncached_storage.py -v
python3 -B test/test_companion_ota_config.py -v
+18 -13
View File
@@ -1060,7 +1060,15 @@ MyMesh::AckTableEntry* MyMesh::findPendingTextMessage(
return NULL;
}
ContactInfo* MyMesh::processAck(const uint8_t *data) {
void MyMesh::onContactReferenceChanged(const ContactInfo* previous, ContactInfo* replacement) {
for (AckTableEntry* entry = expected_ack_table;
entry != expected_ack_table + EXPECTED_ACK_TABLE_SIZE; ++entry) {
if (entry->ack != 0 && entry->contact == previous) entry->contact = replacement;
}
}
bool MyMesh::processAck(const uint8_t *data, ContactInfo*& peer) {
peer = NULL;
expireExpectedAcks();
uint32_t incoming;
memcpy(&incoming, data, sizeof(incoming));
@@ -1090,10 +1098,11 @@ ContactInfo* MyMesh::processAck(const uint8_t *data) {
if (contact != NULL && contact->type == ADV_TYPE_CHAT) rememberOneKeyAck(*contact);
#endif
expireExpectedAcks();
return contact;
peer = contact;
return true;
}
}
return checkConnectionsAck(data);
return checkConnectionsAck(data, peer);
}
#if MESH_ENABLE_ONE_KEY_DM
@@ -1260,7 +1269,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret,
// A prior anonymous service request may occupy a transient slot for this
// key. Remove it so regular text lookup finds the durable contact first.
ContactInfo* transient = lookupTransientContactByPubKey(sender.pub_key, PUB_KEY_SIZE);
if (transient != NULL) clearTransientContact(*transient);
if (transient != NULL) clearTransientContact(*transient, added);
onDiscoveredContact(*added, true, OUT_PATH_UNKNOWN, NULL);
}
@@ -5583,7 +5592,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (scheduleContactWrite(*recipient)) {
ContactInfo* transient = lookupTransientContactByPubKey(
pub_key, PUB_KEY_SIZE);
if (transient != NULL) clearTransientContact(*transient);
if (transient != NULL) clearTransientContact(*transient, recipient);
writeOKFrame();
} else {
recipient->out_path_len = previous_out_path_len;
@@ -5613,8 +5622,6 @@ void MyMesh::handleCmdFrame(size_t len) {
ContactInfo* transient = lookupTransientContactByPubKey(
pub_key, PUB_KEY_SIZE);
ContactInfo previous_transient = {};
if (transient != NULL) previous_transient = *transient;
if (recipient != NULL) {
const ContactInfo previous = *recipient;
@@ -5622,7 +5629,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (scheduleContactWrite(*recipient)) {
// A prior anonymous request can leave a same-key entry in the
// reserved prefix. Retire it in place; never compact that prefix.
if (transient != NULL) clearTransientContact(*transient);
if (transient != NULL) clearTransientContact(*transient, recipient);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
@@ -5630,22 +5637,20 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else {
// Promotion must free the exact transient prefix slot before lookup
// of the newly-added persistent record. Restore it on every failure.
if (transient != NULL) clearTransientContact(*transient);
// Preserve the transient and its pending ACK references until the
// persistent destination is accepted. Failure preserves the transient.
if (addContact(candidate)) {
ContactInfo* added = lookupPersistentContactByPubKey(
candidate.id.pub_key, PUB_KEY_SIZE);
if (added != NULL && scheduleContactWrite(*added)) {
if (transient != NULL) clearTransientContact(*transient, added);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
if (added != NULL) removeContact(*added);
if (transient != NULL) *transient = previous_transient;
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
} else {
if (transient != NULL) *transient = previous_transient;
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
+2 -1
View File
@@ -370,13 +370,14 @@ protected:
bool canMutateContacts() const override;
void onContactsFull() override;
bool onContactOverwrite(const ContactInfo& contact) override;
void onContactReferenceChanged(const ContactInfo* previous, ContactInfo* replacement) override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;
#if COMPANION_FEATURE_TEXT_TERMINAL
void onContactVisit(const ContactInfo& contact) override;
#endif
ContactInfo* processAck(const uint8_t *data) override;
bool processAck(const uint8_t *data, ContactInfo*& peer) override;
#if MESH_ENABLE_ONE_KEY_DM
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret,
const mesh::Identity& sender, uint8_t* data,
+5 -4
View File
@@ -240,18 +240,19 @@ protected:
saveContacts();
}
ContactInfo* processAck(const uint8_t *data) override {
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
bool processAck(const uint8_t *data, ContactInfo*& peer) override {
peer = NULL;
if (expected_ack_crc != 0 && memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
// NOTE: the same ACK can be received multiple times!
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
return NULL; // TODO: really should return ContactInfo pointer
return true; // owned, even without a retained contact pointer
}
//uint32_t crc;
//memcpy(&crc, data, 4);
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
return NULL;
return false;
}
void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
+34 -16
View File
@@ -24,6 +24,12 @@ bool BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Pack
return sendFlood(pkt, delay_millis);
}
// Share the value reset without replacing ContactInfo's assignment/RAII rules.
__attribute__((noinline))
void BaseChatMesh::resetContactValue(ContactInfo& contact) {
contact = ContactInfo();
}
bool BaseChatMesh::initializeContactStorage() {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
const int requested_capacity = MAX_CONTACTS + MAX_ANON_CONTACTS;
@@ -48,10 +54,14 @@ bool BaseChatMesh::initializeContactStorage() {
for (int i = 0; i < requested_capacity; ++i) new (&expanded_contacts[i]) ContactInfo();
for (int i = 0; i < num_contacts; ++i) {
expanded_contacts[i] = contacts[i];
contacts[i] = ContactInfo();
onContactReferenceChanged(&contacts[i], &expanded_contacts[i]);
resetContactValue(contacts[i]);
}
#else
memcpy(expanded_contacts, contacts, sizeof(ContactInfo) * num_contacts);
for (int i = 0; i < num_contacts; ++i) {
onContactReferenceChanged(&contacts[i], &expanded_contacts[i]);
}
#endif
contacts = expanded_contacts;
sort_array = expanded_sort_array;
@@ -145,6 +155,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
}
if (oldest_idx >= 0) {
// NOTE: do NOT call onContactOverwrite()
onContactReferenceChanged(&contacts[oldest_idx], NULL);
return &contacts[oldest_idx];
}
} else {
@@ -169,6 +180,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
if (!onContactOverwrite(contacts[oldest_idx])) {
return NULL;
}
onContactReferenceChanged(&contacts[oldest_idx], NULL);
contact_table_revision++;
return &contacts[oldest_idx];
}
@@ -178,7 +190,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
}
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
ci = ContactInfo();
resetContactValue(ci);
#if defined(NRF52_PLATFORM)
ci.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
@@ -228,11 +240,9 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
packet->header = save;
}
ContactInfo* transient = NULL;
if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ?
*from = ContactInfo(); // release the anon/temp path as well
#if defined(NRF52_PLATFORM)
from->storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
transient = from; // preserve its references if filters/allocation refuse promotion
from = NULL; // do normal 'add' flow
}
@@ -283,6 +293,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
if (transient != NULL) clearTransientContact(*transient, from);
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
}
@@ -470,7 +481,8 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
if (processAck(extra) != NULL) {
ContactInfo* peer;
if (processAck(extra, peer)) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
}
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
@@ -481,11 +493,11 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
ContactInfo* from;
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
if (processAck((uint8_t *)&ack_crc, from)) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
if (from != NULL && packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
// we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
handleReturnPathRetry(*from, packet->path, packet->path_len);
}
@@ -932,9 +944,11 @@ void BaseChatMesh::markConnectionActive(const ContactInfo& contact) {
}
}
ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
bool BaseChatMesh::checkConnectionsAck(const uint8_t* data, ContactInfo*& peer) {
peer = NULL;
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) {
if (connections[i].keep_alive_millis > 0 && connections[i].expected_ack != 0
&& memcmp(&connections[i].expected_ack, data, 4) == 0) {
// yes, got an ack for our keep_alive request!
connections[i].expected_ack = 0;
connections[i].last_activity = getRTCClock()->getCurrentTime();
@@ -943,10 +957,11 @@ ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
auto id = &connections[i].server_id;
return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match
peer = lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE);
return true; // owned even if its contact was removed
}
}
return NULL; /// no match
return false; /// no match
}
void BaseChatMesh::checkConnections() {
@@ -1069,9 +1084,10 @@ bool BaseChatMesh::isTransientContact(const ContactInfo& contact) const {
return false;
}
bool BaseChatMesh::clearTransientContact(ContactInfo& contact) {
bool BaseChatMesh::clearTransientContact(ContactInfo& contact, ContactInfo* replacement) {
if (!isTransientContact(contact)) return false;
contact = ContactInfo();
onContactReferenceChanged(&contact, replacement);
resetContactValue(contact);
#if defined(NRF52_PLATFORM)
contact.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
@@ -1098,12 +1114,14 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
if (idx >= num_contacts) return false; // not found
// remove from contacts array
onContactReferenceChanged(&contacts[idx], NULL);
num_contacts--;
while (idx < num_contacts) {
contacts[idx] = contacts[idx + 1];
onContactReferenceChanged(&contacts[idx + 1], &contacts[idx]);
idx++;
}
contacts[num_contacts] = ContactInfo(); // release the retired tail path
resetContactValue(contacts[num_contacts]); // release the retired tail path
contact_table_revision++;
return true; // Success
}
+24 -6
View File
@@ -108,8 +108,15 @@ protected:
sort_array = sort_array_fallback;
contact_capacity = CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS;
#endif
contact_table_revision = 0;
resetContacts();
// No live references exist during base construction. Seed the initially
// empty prefix directly; runtime resets also notify reference owners.
#if defined(NRF52_PLATFORM)
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
contacts[i].storage_slot = mesh::storage::CONTACT_SLOT_NONE;
}
#endif
num_contacts = MAX_ANON_CONTACTS;
contact_table_revision = 1;
#ifdef MAX_GROUP_CHANNELS
memset(channels, 0, sizeof(channels));
@@ -122,9 +129,13 @@ protected:
void bootstrapRTCfromContacts();
bool initializeContactStorage();
static void resetContactValue(ContactInfo& contact);
void resetContacts() {
for (int i = 0; i < num_contacts; ++i) contacts[i] = ContactInfo();
for (int i = 0; i < num_contacts; ++i) {
onContactReferenceChanged(&contacts[i], NULL);
resetContactValue(contacts[i]);
}
#if defined(NRF52_PLATFORM)
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
contacts[i].storage_slot = mesh::storage::CONTACT_SLOT_NONE;
@@ -147,8 +158,14 @@ protected:
// safely retire its associated persistent state. A refusal must not mutate
// the contact or its backing state.
virtual bool onContactOverwrite(const ContactInfo&) { return true; };
// A replacement is already initialized with the same identity. Notify after
// copying it, before the old slot is cleared/reused; NULL retires an identity.
// Subclasses that retain no contact pointers need no relocation work.
virtual void onContactReferenceChanged(const ContactInfo*, ContactInfo*) {}
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0;
// Ownership does not require a surviving contact. Set peer to NULL when the
// expected ACK belongs to us but its recipient was removed or overwritten.
virtual bool processAck(const uint8_t *data, ContactInfo*& peer) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
@@ -190,7 +207,7 @@ protected:
void stopConnection(const uint8_t* pub_key);
bool hasConnectionTo(const uint8_t* pub_key);
void markConnectionActive(const ContactInfo& contact);
ContactInfo* checkConnectionsAck(const uint8_t* data);
bool checkConnectionsAck(const uint8_t* data, ContactInfo*& peer);
void checkConnections();
public:
@@ -221,7 +238,8 @@ public:
ContactInfo* lookupTransientContactByPubKey(const uint8_t* pub_key, int prefix_len);
ContactInfo* lookupPersistentContactByPubKey(const uint8_t* pub_key, int prefix_len);
bool isTransientContact(const ContactInfo& contact) const;
bool clearTransientContact(ContactInfo& contact);
// Supply a live same-identity replacement when promoting a transient peer.
bool clearTransientContact(ContactInfo& contact, ContactInfo* replacement = NULL);
bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact);
int getTotalContactSlots() const { return num_contacts; }
+11 -3
View File
@@ -72,11 +72,12 @@ public:
return retry_active[key[0]];
}
void rememberOneKeyAck(ContactInfo&) { ++one_key_notices; }
ContactInfo* checkConnectionsAck(const uint8_t* input) {
bool checkConnectionsAck(const uint8_t* input, ContactInfo*& peer) {
++connection_ack_calls;
last_connection_ack_input = input;
memcpy(&last_connection_ack, input, sizeof(last_connection_ack));
return last_connection_ack == connection_ack ? connection_ack_contact : nullptr;
peer = last_connection_ack == connection_ack ? connection_ack_contact : nullptr;
return peer != nullptr;
}
bool hasTerminalOutput() const { return true; }
Terminal& terminalOutput() { return terminal; }
@@ -89,7 +90,14 @@ public:
void clearExpectedAck(AckTableEntry&, bool = true);
void expireExpectedAcks();
AckTableEntry* findPendingTextMessage(const uint8_t*, uint32_t);
ContactInfo* processAck(const uint8_t*);
bool processAck(const uint8_t*, ContactInfo*&);
// Existing delivery scenarios inspect the optional peer; the lifetime suite
// separately exercises ownership with a NULL peer through BaseChatMesh.
ContactInfo* processAck(const uint8_t* input) {
ContactInfo* peer = nullptr;
processAck(input, peer);
return peer;
}
void cancelSerialOperationsForRoute(BaseSerialInterface*);
bool hasFiniteDelayedReplyForRoute(BaseSerialInterface*) const;
void service();
@@ -0,0 +1,2 @@
#pragma once
#include "../../../../src/Identity.h"
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Keep the real identities, packets and contact/path-cache types while the
// source-linked fixture supplies only radio/clock/storage hardware boundaries.
#include <Identity.h>
#include <Packet.h>
@@ -0,0 +1,2 @@
#pragma once
#include "../../../../src/Utils.h"
+650
View File
@@ -0,0 +1,650 @@
#include <Arduino.h>
#include <helpers/ArduinoSerialInterface.h>
#include <helpers/MultiSerialInterface.h>
#include <helpers/ContactInfo.h>
#include <helpers/ContactListOrder.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/PersistentStoreFormat.h>
#include <algorithm>
#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <new>
#include <set>
#include <string>
#include <vector>
#define MAX_ANON_CONTACTS 8
#define MAX_CONTACTS 20
#define MAX_CONNECTIONS 16
#define CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS (MAX_ANON_CONTACTS + 16)
#define ADV_TYPE_NONE 0
#define ADV_TYPE_CHAT 1
#include "production_constants.inc"
#include "production_types.inc"
#include "production_stream.inc"
#include "production_constructors.inc"
#if MESH_CONTACT_CACHE
namespace mesh {
ContactPathStorage& contactPathStorage() {
static ContactPathPool<128, 16> pool;
return pool;
}
}
#endif
static unsigned allocations = 0, fail_allocation = 0;
static std::set<void*> outstanding_allocations;
static constexpr int MALLOC_CAP_SPIRAM = 1, MALLOC_CAP_8BIT = 2;
static void* heap_caps_calloc(size_t count, size_t size, int) {
++allocations;
if (allocations == fail_allocation) return nullptr;
void* result = calloc(count, size);
if (result != nullptr) outstanding_allocations.insert(result);
return result;
}
static void heap_caps_free(void* pointer) {
assert(outstanding_allocations.erase(pointer) == 1);
free(pointer);
}
struct Clock { unsigned long getMillis() const { return millis(); } };
struct RTC { uint32_t getCurrentTime() const { return 100; } };
struct Store {
bool delete_ok = true, incomplete = false;
unsigned deletions = 0;
bool deleteBlobByKey(const uint8_t*, size_t) { ++deletions; return delete_ok; }
bool hasIncompleteContactLoad() const { return incomplete; }
};
struct Terminal {
std::string text;
void print(const char* value) { text += value; }
void printf(const char* format, ...) {
char bytes[160];
va_list args;
va_start(args, format);
vsnprintf(bytes, sizeof(bytes), format, args);
va_end(args);
text += bytes;
}
};
static ContactInfo makeContact(uint8_t key, uint8_t type = ADV_TYPE_CHAT) {
ContactInfo result;
memset(result.id.pub_key, key, sizeof(result.id.pub_key));
result.type = type;
result.lastmod = key + 100;
result.out_path_len = OUT_PATH_UNKNOWN;
snprintf(result.name, sizeof(result.name), "peer-%u", key);
if (key <= 3) {
const uint8_t path[] = {key};
assert(result.setPath(path, 1));
}
return result;
}
class BaseChatMesh {
public:
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
ContactInfo contacts_fallback[MAX_ANON_CONTACTS + 16];
int sort_array_fallback[MAX_ANON_CONTACTS + 16] = {};
ContactInfo* contacts = contacts_fallback;
int* sort_array = sort_array_fallback;
int contact_capacity = MAX_ANON_CONTACTS + 16;
#else
ContactInfo contacts[MAX_ANON_CONTACTS + MAX_CONTACTS];
#endif
int num_contacts = 0;
uint32_t contact_table_revision = 0;
uint32_t txt_send_timeout = 0;
Clock clock;
Clock* _ms = &clock;
RTC rtc;
ConnectionInfo connections[MAX_CONNECTIONS] = {};
#ifdef MAX_GROUP_CHANNELS
ChannelDetails channels[MAX_GROUP_CHANNELS];
int num_channels;
#endif
mesh::Packet* _pendingLoopback = nullptr;
uint8_t temp_buf[MAX_TRANS_UNIT] = {};
bool auto_add = true, overwrite = true, can_mutate = true;
uint8_t max_hops = 0;
unsigned discoveries = 0, full_notices = 0, path_updates = 0;
std::vector<uint8_t> path_notices;
BaseChatMesh() {
#include "production_base_constructor.inc"
}
virtual ~BaseChatMesh() {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
if (contacts != contacts_fallback) {
for (int i = 0; i < contact_capacity; ++i) contacts[i].~ContactInfo();
heap_caps_free(contacts);
heap_caps_free(sort_array);
}
#endif
}
virtual void onContactReferenceChanged(const ContactInfo*, ContactInfo*) {}
virtual bool onContactOverwrite(const ContactInfo&) = 0;
virtual bool processAck(const uint8_t*, ContactInfo*&) = 0;
bool shouldOverwriteWhenFull() const { return overwrite; }
bool shouldAutoAddContactType(uint8_t) const { return auto_add; }
bool canMutateContacts() const { return can_mutate; }
uint8_t getAutoAddMaxHops() const { return max_hops; }
RTC* getRTCClock() { return &rtc; }
bool putBlobByKey(const uint8_t*, int, const uint8_t*, int) { return true; }
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) { ++discoveries; }
void onContactsFull() { ++full_notices; }
void onContactPathUpdated(const ContactInfo&) { ++path_updates; }
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) {}
void handleReturnPathRetry(const ContactInfo& peer, const uint8_t*, uint8_t) {
path_notices.push_back(peer.id.pub_key[0]);
}
void resetContacts();
static void resetContactValue(ContactInfo&);
bool initializeContactStorage();
ContactInfo* allocateContactSlot(bool = false);
void populateContactFromAdvert(ContactInfo&, const mesh::Identity&, const AdvertDataParser&, uint32_t);
void onAdvertRecv(mesh::Packet*, const mesh::Identity&, uint32_t, const uint8_t*, size_t);
ContactInfo* lookupContactByPubKey(const uint8_t*, int);
ContactInfo* lookupTransientContactByPubKey(const uint8_t*, int);
ContactInfo* lookupPersistentContactByPubKey(const uint8_t*, int);
bool isTransientContact(const ContactInfo&) const;
bool clearTransientContact(ContactInfo&, ContactInfo* = nullptr);
bool addContact(const ContactInfo&);
bool removeContact(ContactInfo&);
bool checkConnectionsAck(const uint8_t*, ContactInfo*&);
bool onContactPathRecv(ContactInfo&, uint8_t*, uint8_t, uint8_t*, uint8_t, uint8_t, uint8_t*, uint8_t);
void onAckRecv(mesh::Packet*, uint32_t);
bool millisHasNowPassed(unsigned long) const;
unsigned long futureMillis(int) const;
};
class MyMesh : public BaseChatMesh {
public:
BaseSerialInterface* _serial = nullptr;
Store store;
Store* _store = &store;
Terminal terminal;
uint8_t out_frame[MAX_FRAME_SIZE] = {}, cmd_frame[MAX_FRAME_SIZE] = {};
#include "production_entry.inc"
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE] = {};
bool has_next_ack_expiry = false;
unsigned long next_ack_expiry = 0;
bool retry_active[256] = {};
unsigned cancellations = 0, references_changed = 0;
std::vector<uint8_t> learned;
bool release_ok = true, restore_ok = true, schedule_ok = true;
unsigned releases = 0, restores = 0, schedules = 0;
unsigned ok_replies = 0, error_replies = 0;
uint8_t last_error = 0;
bool cancelActiveRetries(const uint8_t* key) {
++cancellations; retry_active[key[0]] = false; return true;
}
bool hasActiveRetries(const uint8_t* key) const { return retry_active[key[0]]; }
void rememberOneKeyAck(ContactInfo& peer) { learned.push_back(peer.id.pub_key[0]); }
bool hasTerminalOutput() const { return true; }
Terminal& terminalOutput() { return terminal; }
bool scheduleContactWriteAfterRelease(const ContactInfo&, uint16_t& slot) {
++releases; slot = 7; return release_ok;
}
bool restoreContactWriteAfterRelease(const ContactInfo&, uint16_t) { ++restores; return restore_ok; }
bool scheduleContactWrite(const ContactInfo&) { ++schedules; return schedule_ok; }
void updateGpsTelemetryPolicy() {}
void writeOKFrame() { ++ok_replies; }
void writeErrFrame(uint8_t error) { ++error_replies; last_error = error; }
void onContactReferenceChanged(const ContactInfo* old, ContactInfo* replacement) override {
++references_changed;
if (replacement != nullptr) assert(old->id.matches(replacement->id));
applyContactReferenceChanged(old, replacement);
}
void applyContactReferenceChanged(const ContactInfo*, ContactInfo*);
bool onContactOverwrite(const ContactInfo&) override;
bool processAck(const uint8_t*, ContactInfo*&) override;
void clearExpectedAck(AckTableEntry&, bool = true);
void expireExpectedAcks();
bool updateContactFromFrame(ContactInfo&, uint32_t&, const uint8_t*, int);
void handleContactCommand(int);
void acceptVerifiedOneKey(const mesh::Identity&);
void service();
};
#include "production_functions.inc"
struct Fixture {
BufferStream usb_stream, other_stream;
ArduinoSerialInterface usb, other;
MultiSerialInterface manager;
MyMesh node;
Fixture() {
g_mock_millis = 100;
allocations = fail_allocation = 0;
assert(outstanding_allocations.empty());
assert(node.references_changed == 0);
assert(node.num_contacts == MAX_ANON_CONTACTS && node.contact_table_revision == 1);
assert(node.txt_send_timeout == 0 && node._pendingLoopback == nullptr);
for (const auto& connection : node.connections) {
assert(connection.keep_alive_millis == 0 && connection.expected_ack == 0);
assert(connection.next_ping == 0 && connection.last_activity == 0);
}
#ifdef MAX_GROUP_CHANNELS
assert(node.num_channels == 0);
for (const auto& channel : node.channels) {
for (uint8_t byte : channel.channel.secret) assert(byte == 0);
for (char byte : channel.name) assert(byte == 0);
assert(channel.channel.tx_radio == mesh::RADIO_TX_AUTO);
}
#endif
for (int i = 0; i < MAX_ANON_CONTACTS; ++i) {
assert(node.contacts[i].type == ADV_TYPE_NONE && node.contacts[i].id.pub_key[0] == 0);
#if defined(NRF52_PLATFORM)
assert(node.contacts[i].storage_slot == mesh::storage::CONTACT_SLOT_NONE);
#endif
}
usb.begin(usb_stream); usb.enableFlowControl(true);
other.begin(other_stream); other.enableFlowControl(true);
assert(manager.addInterface(InterfaceType::USB, &usb));
assert(manager.addInterface(InterfaceType::HardwareSerial, &other));
manager.enable();
node._serial = &manager;
}
ContactInfo* add(uint8_t key) {
const ContactInfo contact = makeContact(key);
assert(node.addContact(contact));
return node.lookupPersistentContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
}
ContactInfo* transient(uint8_t key) {
const ContactInfo contact = makeContact(key, ADV_TYPE_NONE);
assert(node.addContact(contact));
return node.lookupTransientContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
}
void fill() {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
const int capacity = node.contact_capacity;
#else
const int capacity = MAX_ANON_CONTACTS + MAX_CONTACTS;
#endif
while (node.num_contacts < capacity) add(uint8_t(node.num_contacts + 10));
}
MyMesh::AckTableEntry& pending(ContactInfo* peer, unsigned slot = 0,
uint32_t ack = 0x11223344, BaseSerialInterface* route = nullptr) {
auto& entry = node.expected_ack_table[slot];
entry.ack = ack; entry.msg_sent = 50; entry.expires_at = 10000;
entry.message_timestamp = 1; entry.contact = peer;
entry.reply_route = route == nullptr ? &usb : route;
entry.retry_key[0] = uint8_t(slot + 1); node.retry_active[slot + 1] = true;
entry.text_fingerprint[0] = 0xaa;
return entry;
}
void blockUsb() {
usb_stream.write_capacity = 0;
const uint8_t required = 0;
for (unsigned i = 0; i < 4; ++i) assert(manager.writeFrameToRoute(&usb, &required, 1) == 1);
}
bool ack(uint32_t crc, ContactInfo*& peer) {
return node.processAck(reinterpret_cast<const uint8_t*>(&crc), peer);
}
void received(uint32_t crc) {
mesh::Packet packet;
packet.header = ROUTE_TYPE_FLOOD;
node.txt_send_timeout = 9000;
node.onAckRecv(&packet, crc);
assert(packet.isMarkedDoNotRetransmit() && node.txt_send_timeout == 0);
// Actual Packet marking replaces the route header. Preserve the current
// (separately audited) no-return-path behavior rather than a fake Packet.
assert(node.path_notices.empty());
}
void drain() {
usb_stream.write_capacity = other_stream.write_capacity = 4096;
for (unsigned i = 0; i < 8; ++i) manager.loop();
}
void command(uint8_t key, uint8_t code = CMD_ADD_UPDATE_CONTACT) {
memset(node.cmd_frame, 0, sizeof(node.cmd_frame));
node.cmd_frame[0] = code;
memset(node.cmd_frame + 1, key, PUB_KEY_SIZE);
if (code == CMD_REMOVE_CONTACT) { node.handleContactCommand(1 + PUB_KEY_SIZE); return; }
node.cmd_frame[1 + PUB_KEY_SIZE] = ADV_TYPE_CHAT;
node.cmd_frame[1 + PUB_KEY_SIZE + 2] = OUT_PATH_UNKNOWN;
memcpy(node.cmd_frame + 1 + PUB_KEY_SIZE + 3 + MAX_PATH_SIZE, "updated", 8);
node.handleContactCommand(CONTACT_UPDATE_FRAME_MIN_LEN);
}
void advert(uint8_t key, uint8_t hops = 0) {
ContactInfo contact = makeContact(key);
uint8_t data[MAX_ADVERT_DATA_SIZE];
const size_t size = AdvertDataBuilder(ADV_TYPE_CHAT, "new-advert").encodeTo(data);
mesh::Packet packet;
packet.header = ROUTE_TYPE_FLOOD; packet.path_len = hops;
node.onAdvertRecv(&packet, contact.id, 100, data, size);
}
};
static void unchanged(const MyMesh::AckTableEntry& current, const MyMesh::AckTableEntry& previous) {
assert(current.ack == previous.ack && current.msg_sent == previous.msg_sent);
assert(current.expires_at == previous.expires_at && current.confirmed == previous.confirmed);
assert(current.message_timestamp == previous.message_timestamp && current.reply_route == previous.reply_route);
assert(memcmp(current.retry_key, previous.retry_key, sizeof(current.retry_key)) == 0);
assert(memcmp(current.text_fingerprint, previous.text_fingerprint, sizeof(current.text_fingerprint)) == 0);
#if COMPANION_FEATURE_TEXT_TERMINAL
assert(current.terminal_origin == previous.terminal_origin);
#endif
}
static unsigned confirmations(const std::vector<uint8_t>& bytes, uint32_t ack = 0x11223344) {
unsigned count = 0;
size_t offset = 0;
while (offset < bytes.size()) {
assert(offset + 3 <= bytes.size() && bytes[offset] == '>');
const size_t size = bytes[offset + 1] | (size_t(bytes[offset + 2]) << 8);
offset += 3;
assert(offset + size <= bytes.size());
if (bytes[offset] == PUSH_CODE_SEND_CONFIRMED) {
uint32_t crc, rtt;
assert(size == 9);
memcpy(&crc, bytes.data() + offset + 1, 4);
memcpy(&rtt, bytes.data() + offset + 5, 4);
if (crc == ack) { assert(rtt == 50); ++count; }
}
offset += size;
}
return count;
}
int main() {
unsigned checks = 0;
{
Fixture f;
// Execute the real constructor body without a live reference callback,
// then prove a later runtime reset uses the hook and advances revision.
assert(f.node.references_changed == 0 && f.node.contact_table_revision == 1);
f.node.resetContacts();
assert(f.node.references_changed == MAX_ANON_CONTACTS);
assert(f.node.num_contacts == MAX_ANON_CONTACTS && f.node.contact_table_revision == 2);
++checks;
}
{
Fixture f;
auto* a = f.add(1); auto* b = f.add(2); auto* c = f.add(3);
auto& first = f.pending(b); auto& second = f.pending(c, 1, 0x55667788);
auto& same_peer = f.pending(b, 2, 0x88776655);
const auto prior = first;
assert(f.node.removeContact(*a));
assert(first.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
assert(second.contact == &f.node.contacts[MAX_ANON_CONTACTS + 1]);
assert(same_peer.contact == first.contact);
unchanged(first, prior);
assert(first.contact->id.pub_key[0] == 2 && second.contact->id.pub_key[0] == 3);
uint8_t path[MAX_PATH_SIZE];
assert(first.contact->copyPathTo(path) && path[0] == 2);
assert(second.contact->copyPathTo(path) && path[0] == 3);
f.received(first.ack); f.received(second.ack); f.received(same_peer.ack);
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{2, 3, 2}));
else assert(f.node.learned.empty());
++checks;
}
{
Fixture f; auto* peer = f.add(1); f.blockUsb();
auto& entry = f.pending(peer); const auto prior = entry;
assert(f.node.removeContact(*peer) && entry.contact == nullptr);
unchanged(entry, prior); assert(f.node.retry_active[1]);
f.received(entry.ack);
assert(entry.confirmed && entry.reply_route == &f.usb && entry.msg_sent == 50);
assert(entry.expires_at == 10100 && f.node.cancellations == 1 && f.node.learned.empty());
f.drain(); g_mock_millis = 110; f.node.service(); f.drain();
assert(entry.ack == 0 && confirmations(f.usb_stream.output) == 1);
assert(f.other_stream.output.empty());
++checks;
}
{
Fixture f; f.add(1); auto* tail = f.add(2);
auto& entry = f.pending(tail); const auto prior = entry;
assert(f.node.removeContact(*tail) && entry.contact == nullptr);
unchanged(entry, prior);
f.add(3); assert(entry.contact == nullptr);
f.received(entry.ack); assert(f.node.learned.empty());
++checks;
}
{
Fixture f; auto* peer = f.add(1); auto* anon = f.transient(9);
auto& entry = f.pending(peer); auto& transient_entry = f.pending(anon, 1, 0x55667788);
const auto prior = entry;
f.node.resetContacts();
assert(entry.contact == nullptr && transient_entry.contact == nullptr);
unchanged(entry, prior); f.add(2); assert(entry.contact == nullptr);
f.received(entry.ack); assert(f.node.learned.empty());
++checks;
}
{
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
ContactInfo external = makeContact(10);
assert(!f.node.clearTransientContact(external) && entry.contact == anon);
assert(f.node.clearTransientContact(*anon) && entry.contact == nullptr);
unchanged(entry, prior); f.received(entry.ack);
assert(f.node.learned.empty());
++checks;
}
{
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
// Every unused transient has lastmod zero; force this exact slot to be the
// oldest reusable slot so stale references cannot attach to another key.
for (int i = 0; i < MAX_ANON_CONTACTS; ++i) f.node.contacts[i].lastmod = 1000;
anon->lastmod = 1;
assert(f.transient(10) == anon && entry.contact == nullptr);
unchanged(entry, prior); f.received(entry.ack); assert(f.node.learned.empty());
++checks;
}
for (unsigned failure = 0; failure < 2; ++failure) {
Fixture f; auto* peer = f.add(1); f.fill();
auto& entry = f.pending(peer); const auto prior = entry;
const auto notifications = f.node.references_changed;
if (failure == 0) f.node.release_ok = false;
else f.node.store.delete_ok = false;
assert(!f.node.addContact(makeContact(99)));
assert(entry.contact == peer && peer->id.pub_key[0] == 1);
unchanged(entry, prior);
assert(f.node.references_changed == notifications && f.node.cancellations == 0);
assert(f.node.restores == (failure == 1 ? 1u : 0u));
++checks;
}
{
Fixture f; auto* peer = f.add(1); f.fill();
auto& entry = f.pending(peer); const auto prior = entry;
assert(f.node.addContact(makeContact(99)) && entry.contact == nullptr);
assert(peer->id.pub_key[0] == 99); unchanged(entry, prior);
f.received(entry.ack); assert(f.node.learned.empty());
++checks;
}
for (unsigned failure = 0; failure < 2; ++failure) {
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
const auto prior_contact = *anon;
if (failure == 0) { f.fill(); f.node.overwrite = false; }
else f.node.schedule_ok = false;
f.command(9);
assert(f.node.error_replies == 1 && f.node.ok_replies == 0);
assert(entry.contact == anon && anon->id.matches(prior_contact.id));
assert(anon->lastmod == prior_contact.lastmod && anon->type == prior_contact.type);
unchanged(entry, prior); assert(f.node.cancellations == 0);
++checks;
}
{
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
f.command(9);
const ContactInfo expected = makeContact(9);
auto* accepted = f.node.lookupPersistentContactByPubKey(expected.id.pub_key, PUB_KEY_SIZE);
assert(accepted != nullptr && entry.contact == accepted && anon->id.pub_key[0] == 0);
assert(f.node.ok_replies == 1 && f.node.error_replies == 0);
unchanged(entry, prior); f.received(entry.ack);
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{9}));
++checks;
}
for (unsigned failure = 0; failure < 2; ++failure) {
Fixture f; auto* persistent = f.add(9); auto* anon = f.transient(9);
auto& entry = f.pending(anon); const auto prior = entry;
f.node.schedule_ok = failure == 0;
f.command(9);
assert(entry.contact == (failure == 0 ? persistent : anon));
assert(f.node.ok_replies == (failure == 0 ? 1u : 0u));
assert(f.node.error_replies == (failure == 1 ? 1u : 0u));
unchanged(entry, prior);
++checks;
}
{
Fixture f; auto* old = f.add(1); auto* next = f.add(2); auto* anon = f.transient(9); f.fill();
auto& old_entry = f.pending(old);
auto& next_entry = f.pending(next, 1, 0x55667788);
auto& anon_entry = f.pending(anon, 2, 0x88776655);
const auto old_state = old_entry, next_state = next_entry, anon_state = anon_entry;
f.node.schedule_ok = false; f.command(9);
assert(f.node.error_replies == 1 && old_entry.contact == nullptr);
assert(next_entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
assert(next_entry.contact->id.pub_key[0] == 2 && anon_entry.contact == anon);
unchanged(old_entry, old_state); unchanged(next_entry, next_state); unchanged(anon_entry, anon_state);
assert(f.node.cancellations == 0);
f.received(old_entry.ack); assert(f.node.learned.empty());
++checks;
}
for (unsigned refusal = 0; refusal < 3; ++refusal) {
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
if (refusal == 0) f.node.auto_add = false;
if (refusal == 1) f.node.max_hops = 1;
if (refusal == 2) { f.fill(); f.node.overwrite = false; }
f.advert(9, refusal == 1 ? 1 : 0);
assert(entry.contact == anon && anon->id.pub_key[0] == 9);
unchanged(entry, prior); assert(f.node.cancellations == 0);
++checks;
}
{
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
f.advert(9);
const ContactInfo identity = makeContact(9);
auto* accepted = f.node.lookupPersistentContactByPubKey(identity.id.pub_key, PUB_KEY_SIZE);
assert(accepted != nullptr && entry.contact == accepted && anon->id.pub_key[0] == 0);
unchanged(entry, prior); f.received(entry.ack);
++checks;
}
for (unsigned failure = 0; failure < 2; ++failure) {
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
const auto prior = entry;
f.node.schedule_ok = failure == 0;
const auto identity = makeContact(9).id;
f.node.acceptVerifiedOneKey(identity);
if (failure == 0) {
assert(entry.contact == f.node.lookupPersistentContactByPubKey(identity.pub_key, PUB_KEY_SIZE));
assert(anon->id.pub_key[0] == 0);
} else assert(entry.contact == anon && anon->id.pub_key[0] == 9);
unchanged(entry, prior);
++checks;
}
for (unsigned failure = 0; failure < 2; ++failure) {
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
const auto prior = entry;
if (failure == 0) f.node.release_ok = false;
else f.node.store.delete_ok = false;
f.command(1, CMD_REMOVE_CONTACT);
assert(f.node.error_replies == 1 && entry.contact == peer && peer->id.pub_key[0] == 1);
unchanged(entry, prior); assert(f.node.cancellations == 0);
++checks;
}
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
for (unsigned failure = 1; failure <= 2; ++failure) {
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
const auto prior = entry;
const auto notifications = f.node.references_changed;
fail_allocation = failure;
assert(!f.node.initializeContactStorage());
assert(entry.contact == peer && f.node.contacts == f.node.contacts_fallback);
assert(outstanding_allocations.empty() && f.node.references_changed == notifications);
unchanged(entry, prior);
++checks;
}
{
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
entry.confirmed = true; entry.msg_sent = 50; entry.expires_at = 10100;
const auto prior = entry;
assert(f.node.initializeContactStorage());
assert(entry.contact != peer && entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
assert(entry.contact->id.pub_key[0] == 1);
uint8_t path[MAX_PATH_SIZE]; assert(entry.contact->copyPathTo(path) && path[0] == 1);
unchanged(entry, prior); assert(f.node.cancellations == 0);
++checks;
}
#endif
{
Fixture f; auto* peer = f.add(1);
auto& connection = f.node.connections[0];
connection.server_id = peer->id; connection.keep_alive_millis = 1000;
connection.last_activity = 1; connection.next_ping = 2;
ContactInfo* result = peer;
assert(!f.ack(0, result) && result == nullptr);
assert(connection.last_activity == 1 && connection.next_ping == 2);
connection.expected_ack = 0x11223344;
assert(f.ack(connection.expected_ack, result) && result == peer);
assert(connection.expected_ack == 0 && connection.last_activity == 100);
result = peer; assert(!f.ack(0x11223344, result) && result == nullptr);
connection.expected_ack = 0x11223344;
assert(f.node.removeContact(*peer));
f.received(connection.expected_ack);
assert(connection.expected_ack == 0 && f.node.learned.empty());
connection.expected_ack = 0x11223344; connection.keep_alive_millis = 0;
assert(!f.ack(0x11223344, result) && result == nullptr);
++checks;
}
{
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
assert(f.node.removeContact(*peer));
ContactInfo sender = makeContact(2); uint8_t path[] = {3};
uint32_t crc = entry.ack;
f.node.can_mutate = false; f.node.txt_send_timeout = 9000;
assert(f.node.onContactPathRecv(sender, path, 1, path, 1, PAYLOAD_TYPE_ACK,
reinterpret_cast<uint8_t*>(&crc), sizeof(crc)));
assert(f.node.txt_send_timeout == 0 && f.node.cancellations == 1 && f.node.learned.empty());
++checks;
}
{
Fixture f; auto* a = f.add(1); auto* b = f.add(2); f.blockUsb();
auto& entry = f.pending(b);
ContactInfo* result = nullptr;
assert(f.ack(entry.ack, result) && result == b && entry.confirmed);
const auto prior = entry;
const auto learned = f.node.learned;
assert(f.node.removeContact(*a));
assert(entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]); unchanged(entry, prior);
assert(!f.ack(entry.ack, result) && result == nullptr);
assert(f.node.removeContact(*entry.contact) && entry.contact == nullptr);
unchanged(entry, prior); assert(f.node.learned == learned && f.node.cancellations == 1);
f.drain(); g_mock_millis = 110; f.node.service(); f.drain();
assert(entry.ack == 0 && confirmations(f.usb_stream.output) == 1);
++checks;
}
{
Fixture f; auto* old = f.add(1); auto* live = f.add(2); f.blockUsb();
auto& first = f.pending(old);
auto& second = f.pending(live, 1, first.ack, &f.other);
assert(f.node.removeContact(*old) && first.contact == nullptr && second.contact->id.pub_key[0] == 2);
ContactInfo* result = live;
assert(f.ack(first.ack, result) && result == nullptr && first.confirmed && !second.confirmed);
assert(f.ack(first.ack, result) && result == &f.node.contacts[MAX_ANON_CONTACTS]);
assert(f.node.cancellations == 2);
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{2}));
else assert(f.node.learned.empty());
++checks;
}
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
assert(checks == 31);
#else
assert(checks == 28);
#endif
assert(outstanding_allocations.empty());
printf("PASS: %u production contact lifetime checks\n", checks);
}
+1 -1
View File
@@ -31,7 +31,7 @@ class CompanionAckDeliveryTests(unittest.TestCase):
functions = "\n".join(extract_braced(source, signature) for signature in (
"void MyMesh::clearExpectedAck(", "void MyMesh::expireExpectedAcks(",
"MyMesh::AckTableEntry* MyMesh::findPendingTextMessage(",
"ContactInfo* MyMesh::processAck(",
"bool MyMesh::processAck(",
"void MyMesh::cancelSerialOperationsForRoute(",
"bool MyMesh::hasFiniteDelayedReplyForRoute(",
))
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Execute production contact mutations and ACK ownership under sanitizers."""
from pathlib import Path
import os
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]
FIXTURE = ROOT / "test/fixtures/companion_contact_lifetime"
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
class CompanionContactLifetimeTests(unittest.TestCase):
def test_production_mutations_and_owned_ack(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
base = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text()
base_header = (ROOT / "src/helpers/BaseChatMesh.h").read_text()
constructor = extract_braced(base_header, "BaseChatMesh(mesh::Radio&")
constructor_body = constructor[constructor.index("{") + 1:-1]
packet = (ROOT / "src/Packet.cpp").read_text()
identity = (ROOT / "src/Identity.cpp").read_text()
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
native = (ROOT / "test/test_serial_mode_switch/test_serial_mode_switch.cpp").read_text()
functions = "\n".join(extract_braced(base, signature) for signature in (
"void BaseChatMesh::resetContactValue(",
"bool BaseChatMesh::initializeContactStorage(",
"void BaseChatMesh::populateContactFromAdvert(",
"void BaseChatMesh::onAdvertRecv(",
"ContactInfo* BaseChatMesh::lookupContactByPubKey(",
"ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(",
"ContactInfo* BaseChatMesh::lookupPersistentContactByPubKey(",
"bool BaseChatMesh::isTransientContact(",
"bool BaseChatMesh::clearTransientContact(",
"bool BaseChatMesh::addContact(",
"bool BaseChatMesh::removeContact(",
"bool BaseChatMesh::checkConnectionsAck(",
"bool BaseChatMesh::onContactPathRecv(",
"void BaseChatMesh::onAckRecv(",
))
# Both preprocessor alternatives contain the same opening brace. Keep
# the complete real definition, rather than selecting a copied branch.
functions += "\n" + base[base.index("ContactInfo* BaseChatMesh::allocateContactSlot("):
base.index("void BaseChatMesh::populateContactFromAdvert(")]
functions += "\nvoid BaseChatMesh::" + extract_braced(base_header, "void resetContacts() {")[5:]
functions += "\n" + extract_braced(
(ROOT / "src/helpers/TxtDataHelpers.cpp").read_text(), "void StrHelper::strncpy(")
functions += "\n" + "\n".join(extract_braced(companion, signature) for signature in (
"void MyMesh::clearExpectedAck(", "void MyMesh::expireExpectedAcks(",
"bool MyMesh::processAck(", "bool MyMesh::onContactOverwrite(",
"bool MyMesh::updateContactFromFrame(",
))
functions += "\n" + extract_braced(companion, "void MyMesh::onContactReferenceChanged(").replace(
"MyMesh::onContactReferenceChanged(", "MyMesh::applyContactReferenceChanged(", 1)
functions += "\n" + "\n".join(extract_braced(dispatcher, signature).replace(
"Dispatcher::", "BaseChatMesh::") for signature in (
"bool Dispatcher::millisHasNowPassed(", "unsigned long Dispatcher::futureMillis("))
functions += "\nvoid MyMesh::service() {\n" + extract_braced(companion, "if (has_next_ack_expiry\n") + "\n}\n"
# Execute the actual admin branches. Framing/authentication is already
# covered separately; the real parser and mutation/rollback are retained.
functions += "\nvoid MyMesh::handleContactCommand(int len) { if (false) {}\n"
functions += extract_braced(companion, "else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT)")
functions += "\n" + extract_braced(companion, "else if (cmd_frame[0] == CMD_REMOVE_CONTACT &&") + "\n}\n"
verified = extract_braced(companion, "void MyMesh::onAnonDataRecv(")
tail_start = verified.index(" if (!canMutateContacts()) return;")
tail_end = verified.rindex("\n}")
functions += "\nvoid MyMesh::acceptVerifiedOneKey(const mesh::Identity& sender) {\n"
functions += " ContactInfo contact = makeContact(sender.pub_key[0]);\n"
functions += verified[tail_start:tail_end] + "\n}\n"
constants = "\n".join(re.findall(
r"^#define (?:EXPECTED_ACK_[A-Z_]+|PUSH_CODE_SEND_CONFIRMED|PUSH_CODE_CONTACT_DELETED|"
r"CMD_ADD_UPDATE_CONTACT|CMD_REMOVE_CONTACT|ERR_CODE_[A-Z_]+)\s+.+$", companion, re.MULTILINE))
constants += "\n#define EXPECTED_ACK_TABLE_SIZE 8\n"
first = companion.index("static constexpr int CONTACT_UPDATE_FRAME_MIN_LEN")
last = companion.index("bool MyMesh::updateContactFromFrame(", first)
constants += "\n" + companion[first:last]
constructors = "namespace mesh {\n" + "\n".join(
extract_braced(identity, signature) for signature in (
"Identity::Identity()", "LocalIdentity::LocalIdentity()")) + "\n"
constructors += "\n".join(extract_braced(packet, signature) for signature in (
"Packet::Packet()", "bool Packet::isValidPathLen(",
"size_t Packet::writePath(", "uint8_t Packet::copyPath(",
"uint8_t Packet::writeTo(")) + "\n}\n"
variants = (("generic", 0, 0, 0), ("generic", 1, 1, 1),
("nrf52", 0, 1, 0), ("nrf52", 1, 1, 1),
("psram", 0, 0, 0), ("psram", 0, 1, 1),
("psram", 1, 0, 1), ("psram", 1, 1, 0))
with tempfile.TemporaryDirectory(prefix="meshcore-contact-lifetime-") as directory:
work = Path(directory)
(work / "production_constants.inc").write_text(constants, encoding="ascii")
(work / "production_types.inc").write_text(
"namespace mesh {\n" + extract_braced((ROOT / "src/Mesh.h").read_text(), "class GroupChannel")
+ ";\n}\n" + extract_braced((ROOT / "src/helpers/ChannelDetails.h").read_text(), "struct ChannelDetails")
+ ";\n" + extract_braced(base_header, "struct ConnectionInfo") + ";\n", encoding="ascii")
(work / "production_entry.inc").write_text(
extract_braced(header, "struct AckTableEntry") + ";\n", encoding="ascii")
(work / "production_stream.inc").write_text(
extract_braced(native, "class BufferStream") + ";\n", encoding="ascii")
(work / "production_functions.inc").write_text(functions, encoding="ascii")
(work / "production_constructors.inc").write_text(constructors, encoding="ascii")
(work / "production_base_constructor.inc").write_text(constructor_body, encoding="ascii")
for platform, cache, terminal, one_key in variants:
with self.subTest(platform=platform, cache=cache, terminal=terminal, one_key=one_key):
defines = [f"-DMESH_CONTACT_CACHE={cache}",
f"-DCOMPANION_FEATURE_TEXT_TERMINAL={terminal}",
f"-DMESH_ENABLE_ONE_KEY_DM={one_key}"]
if terminal:
defines.append("-DMAX_GROUP_CHANNELS=4")
if platform == "nrf52":
defines.append("-DNRF52_PLATFORM=1")
elif platform == "psram":
defines += ["-DESP32_PLATFORM=1", "-DBOARD_HAS_PSRAM=1"]
binary = work / f"lifetime-{platform}-{cache}-{terminal}-{one_key}.exe"
compiled = subprocess.run([
compiler, "-std=c++17", "-Werror", *SANITIZERS, *defines,
f"-I{work}", f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'test/mocks'}",
f"-I{ROOT / 'src'}", str(FIXTURE / "test.cpp"),
str(ROOT / "src/helpers/ArduinoSerialInterface.cpp"),
str(ROOT / "src/helpers/AdvertDataHelpers.cpp"),
"-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
expected_checks = 31 if platform == "psram" else 28
self.assertIn(f"PASS: {expected_checks} production contact lifetime checks", checked.stdout)
if __name__ == "__main__":
unittest.main()
@@ -152,7 +152,7 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
)
error = section.index("writeErrFrame(ERR_CODE_FILE_IO_ERROR)", rollback)
ok = section.index("writeOKFrame()", schedule)
clear_transient = section.index("clearTransientContact(*transient)", schedule)
clear_transient = section.index("clearTransientContact(*transient, recipient)", schedule)
self.assertLess(snapshot, mutation)
self.assertLess(mutation, schedule)
self.assertLess(schedule, clear_transient)
@@ -227,10 +227,11 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
self.assertLess(update_ok, update_rollback)
self.assertLess(update_rollback, update_error)
promotion = section.index("Promotion must free the exact transient prefix slot")
promotion = section.index("Preserve the transient and its pending ACK references")
add_section = section[promotion:]
clear = add_section.index("clearTransientContact(*transient)")
add = add_section.index("addContact(candidate)")
schedule = add_section.index("added != NULL && scheduleContactWrite(*added)")
clear = add_section.index("clearTransientContact(*transient, added)")
add_schedule = section.index(
"added != NULL && scheduleContactWrite(*added)", update_error
)
@@ -239,11 +240,13 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
add_error = section.index(
"writeErrFrame(ERR_CODE_FILE_IO_ERROR)", add_rollback
)
self.assertLess(clear, add)
self.assertLess(add, schedule)
self.assertLess(schedule, clear)
self.assertLess(add_schedule, add_ok)
self.assertLess(add_ok, add_rollback)
self.assertLess(add_rollback, add_error)
self.assertIn("if (transient != NULL) *transient = previous_transient", add_section)
self.assertNotIn("clearTransientContact", add_section[:schedule])
self.assertNotIn("previous_transient", section)
def test_transient_prefix_is_never_compacted(self):
transient_lookup = function_body(
@@ -73,7 +73,7 @@ public:
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {
++discovered;
}
ContactInfo* processAck(const uint8_t*) override { return nullptr; }
bool processAck(const uint8_t*, ContactInfo*& peer) override { peer = nullptr; return false; }
void onContactPathUpdated(const ContactInfo&) override {}
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}