Cache Full Companion paths and shared secrets to recover RAM

This commit is contained in:
mikecarper committed 2026-09-08 12:21:30 -07:00
1 parent 5013c8ff34
commit 6db10c3986
27 files changed
+2360 -58

No files matched your search

+25 -16
View File
@@ -44,7 +44,15 @@ bool BaseChatMesh::initializeContactStorage() {
return false;
}
#if MESH_CONTACT_CACHE
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();
}
#else
memcpy(expanded_contacts, contacts, sizeof(ContactInfo) * num_contacts);
#endif
contacts = expanded_contacts;
sort_array = expanded_sort_array;
contact_capacity = requested_capacity;
@@ -84,12 +92,12 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, u
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, ack_len, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
if (a1) sendDirect(a1, dest.getPath(), dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash, ack_len);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
if (a2) sendDirect(a2, dest.getPath(), dest.out_path_len, d);
}
}
@@ -154,7 +162,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
}
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
memset(&ci, 0, sizeof(ci));
ci = ContactInfo();
#if defined(NRF52_PLATFORM)
ci.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
@@ -205,7 +213,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
}
if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ?
memset(from, 0, sizeof(*from)); // clear the anon/temp slot
*from = ContactInfo(); // release the anon/temp path as well
#if defined(NRF52_PLATFORM)
from->storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
@@ -352,7 +360,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
if (from.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS);
} else {
sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
sendDirect(reply_pkt, from.getPath(), from.out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
@@ -393,7 +401,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
if (reply) {
if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
sendDirect(reply, from.getPath(), from.out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
}
@@ -425,7 +433,7 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
if (canMutateContacts()) {
from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect()
if (!from.setPath(out_path, out_path_len)) return false;
from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from);
@@ -459,7 +467,7 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8
// NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY)
// override this method in various firmwares, if there's a better strategy
mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
if (rpath) sendDirect(rpath, contact.getPath(), contact.out_path_len, 3000); // 3 second delay
}
#ifdef MAX_GROUP_CHANNELS
@@ -565,7 +573,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
est_timeout = calcFloodTimeoutMillisFor(t);
rc = MSG_SEND_SENT_FLOOD;
} else {
sent = sendDirect(pkt, recipient.out_path, recipient.out_path_len);
sent = sendDirect(pkt, recipient.getPath(), recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
rc = MSG_SEND_SENT_DIRECT;
}
@@ -632,7 +640,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
sent = sendFloodScoped(recipient, pkt);
rc = MSG_SEND_SENT_FLOOD;
} else {
sent = sendDirect(pkt, recipient.out_path, recipient.out_path_len);
sent = sendDirect(pkt, recipient.getPath(), recipient.out_path_len);
rc = MSG_SEND_SENT_DIRECT;
}
if (!sent) {
@@ -755,7 +763,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
@@ -780,7 +788,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data,
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
@@ -807,7 +815,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
@@ -834,7 +842,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
if (!sendDirect(pkt, recipient.getPath(), recipient.out_path_len)) return MSG_SEND_FAILED;
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
@@ -950,7 +958,7 @@ void BaseChatMesh::checkConnections() {
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9);
if (pkt) {
sendDirect(pkt, contact->out_path, contact->out_path_len);
sendDirect(pkt, contact->getPath(), contact->out_path_len);
}
// schedule next KEEP_ALIVE
@@ -1033,7 +1041,7 @@ bool BaseChatMesh::isTransientContact(const ContactInfo& contact) const {
bool BaseChatMesh::clearTransientContact(ContactInfo& contact) {
if (!isTransientContact(contact)) return false;
memset(&contact, 0, sizeof(contact));
contact = ContactInfo();
#if defined(NRF52_PLATFORM)
contact.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
@@ -1065,6 +1073,7 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
contacts[idx] = contacts[idx + 1];
idx++;
}
contacts[num_contacts] = ContactInfo(); // release the retired tail path
contact_table_revision++;
return true; // Success
}
+2 -2
View File
@@ -84,7 +84,7 @@ class BaseChatMesh : public mesh::Mesh {
ContactInfo contacts[MAX_CONTACTS+MAX_ANON_CONTACTS];
int sort_array[MAX_CONTACTS+MAX_ANON_CONTACTS];
#endif
int num_contacts;
int num_contacts = 0;
uint32_t contact_table_revision;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
@@ -124,7 +124,7 @@ protected:
bool initializeContactStorage();
void resetContacts() {
memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0)
for (int i = 0; i < num_contacts; ++i) contacts[i] = ContactInfo();
#if defined(NRF52_PLATFORM)
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
contacts[i].storage_slot = mesh::storage::CONTACT_SLOT_NONE;
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// PSRAM boards already keep the complete contact table outside internal RAM.
// An explicit 0/1 override is useful for matched qualification builds.
#ifndef MESH_CONTACT_CACHE
#if defined(COMPANION_RADIO_FULL) && !defined(BOARD_HAS_PSRAM) && \
(defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))
#define MESH_CONTACT_CACHE 1
#else
#define MESH_CONTACT_CACHE 0
#endif
#endif
// A measured ESP32-S3 SPIFFS lookup took ~132 ms versus ~22 ms for key
// exchange. Recalculate cold ESP32 entries instead of spending more time and
// writes on flash. On a RAK3401, LittleFS took ~8 ms versus ~31 ms for key
// exchange, so nRF52 reuses saved secrets when available.
#ifndef MESH_CONTACT_SECRET_FLASH_CACHE
#if defined(NRF52_PLATFORM)
#define MESH_CONTACT_SECRET_FLASH_CACHE 1
#else
#define MESH_CONTACT_SECRET_FLASH_CACHE 0
#endif
#endif
+81
View File
@@ -0,0 +1,81 @@
#pragma once
// SPIFFS cannot replace an existing name in one rename. Preserve the previous
// file as .bak until the verified replacement has its final name. Recovery of
// the rename gap happens before loading contacts on the next boot.
#if defined(ESP32_PLATFORM)
#include "IdentityStore.h"
#include "PersistentStoreFormat.h"
#include <stdio.h>
namespace mesh {
class ContactFileTransaction {
FILESYSTEM* _fs;
const char* _target;
char _temp[48];
char _backup[48];
File _file;
size_t _size = 0;
uint32_t _crc = 0xffffffff;
bool _ok = false;
bool _finished = false;
public:
static bool recover(FILESYSTEM* fs, const char* target) {
char backup[48];
snprintf(backup, sizeof(backup), "%s.bak", target);
if (fs->exists(target)) return true;
return !fs->exists(backup) || fs->rename(backup, target);
}
ContactFileTransaction(FILESYSTEM* fs, const char* target) : _fs(fs), _target(target) {
snprintf(_temp, sizeof(_temp), "%s.tmp", target);
snprintf(_backup, sizeof(_backup), "%s.bak", target);
if (!recover(fs, target)) return;
if (fs->exists(_temp) && !fs->remove(_temp)) return;
_file = fs->open(_temp, "w", true);
_ok = static_cast<bool>(_file);
}
~ContactFileTransaction() {
if (_file) _file.close();
if (!_finished) _fs->remove(_temp);
}
operator bool() const { return _ok; }
size_t write(const uint8_t* data, size_t len) {
if (!_ok) return 0;
const size_t wrote = _file.write(data, len);
_ok = wrote == len;
_size += wrote;
_crc = storage::updateCRC32(_crc, data, wrote);
return wrote;
}
bool commit(bool valid = true) {
if (_finished) return false;
if (_file) { _file.flush(); _file.close(); }
bool ok = _ok && valid;
File verify = _fs->open(_temp, "r");
ok = ok && verify && verify.size() == _size;
uint32_t crc = 0xffffffff;
size_t remaining = _size;
uint8_t buf[64];
while (ok && remaining) {
const size_t count = remaining < sizeof(buf) ? remaining : sizeof(buf);
ok = verify.read(buf, count) == count;
if (ok) { crc = storage::updateCRC32(crc, buf, count); remaining -= count; }
}
if (verify) verify.close();
ok = ok && crc == _crc;
if (ok && _fs->exists(_backup)) ok = _fs->remove(_backup);
bool backed_up = false;
if (ok && _fs->exists(_target)) {
ok = _fs->rename(_target, _backup);
backed_up = ok;
}
if (ok) ok = _fs->rename(_temp, _target);
if (!ok && backed_up) _fs->rename(_backup, _target);
if (ok) _fs->remove(_backup);
else _fs->remove(_temp);
_finished = true;
return ok;
}
};
} // namespace mesh
#endif
+28
View File
@@ -0,0 +1,28 @@
#include "ContactInfo.h"
#if MESH_CONTACT_CACHE
#include "ContactSecretCache.h"
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 32
#endif
namespace mesh {
ContactPathStorage& contactPathStorage() {
// Eight anonymous peers plus room for independent in-flight snapshots.
static ContactPathPool<MAX_CONTACTS + 40> pool;
return pool;
}
ContactSecretCache& contactSecretCache() {
static ContactSecretCache cache;
return cache;
}
} // namespace mesh
const uint8_t* ContactInfo::getSharedSecret(const mesh::LocalIdentity& self_id) const {
return mesh::contactSecretCache().get(self_id, id.pub_key);
}
#endif
+61 -9
View File
@@ -2,6 +2,10 @@
#include <Arduino.h>
#include <Mesh.h>
#include "ContactCachePolicy.h"
#if MESH_CONTACT_CACHE
#include "ContactPathCache.h"
#endif
#if defined(NRF52_PLATFORM)
#include "PersistentStoreFormat.h"
#endif
@@ -10,16 +14,20 @@
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
uint8_t out_path_len;
mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
char name[32] = {};
uint8_t type = 0; // one of ADV_TYPE_*
uint8_t flags = 0;
uint8_t out_path_len = 0;
mutable bool shared_secret_valid = false;
#if MESH_CONTACT_CACHE
mesh::ContactPathRef path_ref;
#else
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
#endif
uint32_t last_advert_timestamp = 0; // by THEIR clock
uint32_t lastmod = 0; // by OUR clock
int32_t gps_lat = 0, gps_lon = 0; // 6 dec places
uint32_t sync_since = 0;
// Runtime-only position in the paged companion contact store. It is not
// part of the on-disk record; the record's page/slot supplies it on load.
@@ -28,6 +36,47 @@ struct ContactInfo {
mutable uint16_t storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
// The pointer is a short-lived borrow. Copy bytes for a queued operation;
// another contact-cache access may replace the resident entry.
const uint8_t* getPath() const {
#if MESH_CONTACT_CACHE
return path_ref.view();
#else
return out_path;
#endif
}
bool copyPathTo(uint8_t path[MAX_PATH_SIZE]) const {
#if MESH_CONTACT_CACHE
return path_ref.read(path);
#else
memcpy(path, out_path, MAX_PATH_SIZE);
return true;
#endif
}
bool setRawPath(const uint8_t path[MAX_PATH_SIZE]) {
#if MESH_CONTACT_CACHE
return path_ref.set(path);
#else
memcpy(out_path, path, MAX_PATH_SIZE);
return true;
#endif
}
bool setPath(const uint8_t* path, uint8_t encoded_len) {
if (encoded_len != OUT_PATH_UNKNOWN &&
!mesh::Packet::isValidPathLen(encoded_len)) return false;
uint8_t normalized[MAX_PATH_SIZE] = {};
if (encoded_len != OUT_PATH_UNKNOWN && (encoded_len & 63)) {
if (!path) return false;
mesh::Packet::copyPath(normalized, path, encoded_len);
}
if (!setRawPath(normalized)) return false;
out_path_len = encoded_len;
return true;
}
#if MESH_CONTACT_CACHE
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const;
#else
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const {
if (!shared_secret_valid) {
self_id.calcSharedSecret(shared_secret, id.pub_key);
@@ -35,6 +84,7 @@ struct ContactInfo {
}
return shared_secret;
}
#endif
bool isFav() const { return flags & 0x01; }
bool isTelemBaseAllowed() const { return flags & 0x02; }
@@ -43,5 +93,7 @@ struct ContactInfo {
bool isRemoteCLIAllowed() const { return flags & 0x10; }
private:
#if !MESH_CONTACT_CACHE
mutable uint8_t shared_secret[PUB_KEY_SIZE];
#endif
};
+267
View File
@@ -0,0 +1,267 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
namespace mesh {
// Paths are immutable values shared by ContactInfo copies. Editing a copy uses
// a new handle, so a pending app response or a rollback keeps its original path.
// Only the 16 most recently used path byte arrays are resident. Small handles
// retain the location of the other paths in the existing contact files.
class ContactPathBackend {
public:
virtual bool readStoredPath(uint16_t source, uint8_t path[64]) = 0;
virtual bool flushCachedPaths() = 0;
};
class ContactPathStorage {
public:
static const uint16_t NONE = 0xffff;
static const uint16_t PAGED = 0x8000;
virtual void attach(ContactPathBackend* backend) = 0;
virtual uint16_t bind(uint16_t source, const uint8_t path[64]) = 0;
virtual void retain(uint16_t handle) = 0;
virtual void release(uint16_t handle) = 0;
virtual bool read(uint16_t handle, uint8_t path[64]) = 0;
virtual const uint8_t* view(uint16_t handle) = 0;
virtual bool replace(uint16_t& handle, const uint8_t path[64]) = 0;
virtual uint16_t source(uint16_t handle) const = 0;
virtual void beginCommit() = 0;
virtual void mark(uint16_t handle) = 0;
virtual bool preserveSnapshots(uint16_t first, uint16_t count) = 0;
virtual void publish(uint16_t handle, uint16_t source) = 0;
virtual void endCommit(bool success) = 0;
};
ContactPathStorage& contactPathStorage();
class ContactPathRef {
uint16_t _handle = 0;
public:
ContactPathRef() = default;
ContactPathRef(const ContactPathRef& other) : _handle(other._handle) {
if (_handle) contactPathStorage().retain(_handle);
}
ContactPathRef& operator=(const ContactPathRef& other) {
if (this != &other) {
if (other._handle) contactPathStorage().retain(other._handle);
if (_handle) contactPathStorage().release(_handle);
_handle = other._handle;
}
return *this;
}
~ContactPathRef() { if (_handle) contactPathStorage().release(_handle); }
bool bind(uint16_t source, const uint8_t path[64]) {
uint16_t next = contactPathStorage().bind(source, path);
if (next == ContactPathStorage::NONE) return false;
if (_handle) contactPathStorage().release(_handle);
_handle = next;
return true;
}
bool set(const uint8_t path[64]) {
return contactPathStorage().replace(_handle, path);
}
bool read(uint8_t path[64]) const {
return contactPathStorage().read(_handle, path);
}
const uint8_t* view() const { return contactPathStorage().view(_handle); }
uint16_t handle() const { return _handle; }
};
template<size_t Handles, size_t Resident = 16>
class ContactPathPool : public ContactPathStorage {
static_assert(Handles > Resident && Handles < 0xffff, "Invalid path handle capacity");
static_assert(Resident > 0 && Resident < 255, "Invalid path cache capacity");
enum : uint8_t { DIRTY = 1, MARKED = 2, DETACH = 4 };
struct Slot {
uint32_t checksum = 0;
uint16_t refs = 0;
uint16_t location = NONE;
uint8_t resident = 255;
uint8_t flags = 0;
} _slots[Handles];
struct Entry {
uint8_t path[64] = {};
uint16_t handle = 0;
uint8_t age = 0;
} _entries[Resident];
ContactPathBackend* _backend = nullptr;
bool _committing = false;
bool _flushing = false;
static uint32_t checksum(const uint8_t path[64]) {
uint32_t crc = 0xffffffff;
for (size_t i = 0; i < 64; ++i) {
crc ^= path[i];
for (unsigned bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320UL : 0);
}
return crc;
}
Slot* slot(uint16_t h) {
return h > 0 && h <= Handles && _slots[h - 1].refs ? &_slots[h - 1] : nullptr;
}
void touch(size_t index) {
// Bounded ranks avoid millis/counter wrap affecting LRU ordering.
for (size_t i = 0; i < Resident; ++i) {
if (_entries[i].handle && _entries[i].age < Resident) ++_entries[i].age;
}
_entries[index].age = 0;
}
int availableEntry() {
int oldest = -1;
for (size_t i = 0; i < Resident; ++i) {
Slot* s = slot(_entries[i].handle);
if (!s) return static_cast<int>(i);
if (s->location != NONE && !(s->flags & (DIRTY | DETACH)) &&
(oldest < 0 || _entries[i].age > _entries[oldest].age)) oldest = i;
}
return oldest;
}
int makeRoom() {
int index = availableEntry();
if (index < 0 && _backend && !_committing && !_flushing) {
_flushing = true;
_backend->flushCachedPaths();
_flushing = false;
index = availableEntry();
}
if (index >= 0) {
Slot* old = slot(_entries[index].handle);
if (old) old->resident = 255;
_entries[index].handle = 0;
}
return index;
}
uint16_t allocate() {
for (size_t i = 0; i < Handles; ++i) {
if (!_slots[i].refs) {
_slots[i] = Slot();
_slots[i].refs = 1;
return static_cast<uint16_t>(i + 1);
}
}
return NONE;
}
int load(uint16_t h) {
Slot* s = slot(h);
if (!s) return -1;
if (s->resident != 255) { touch(s->resident); return s->resident; }
// A flush in makeRoom() may move this source. Read its current location
// afterward. read() itself never allocates or starts a persistence write.
if (!_backend || s->location == NONE) return -1;
int index = makeRoom();
if (index < 0) return -1;
if (!_backend->readStoredPath(s->location, _entries[index].path) ||
checksum(_entries[index].path) != s->checksum) return -1;
_entries[index].handle = h;
s->resident = index;
touch(index);
return index;
}
public:
void attach(ContactPathBackend* backend) override { _backend = backend; }
uint16_t bind(uint16_t location, const uint8_t path[64]) override {
if (location == NONE) return NONE;
const uint16_t h = allocate();
if (h != NONE) {
_slots[h - 1].location = location;
_slots[h - 1].checksum = checksum(path);
}
return h;
}
void retain(uint16_t h) override { Slot* s = slot(h); if (s) ++s->refs; }
void release(uint16_t h) override {
Slot* s = slot(h);
if (!s || --s->refs) return;
if (s->resident != 255) _entries[s->resident].handle = 0;
*s = Slot();
}
bool read(uint16_t h, uint8_t path[64]) override {
if (!h) { memset(path, 0, 64); return true; }
Slot* s = slot(h);
if (!s) return false;
if (s->resident != 255) {
memcpy(path, _entries[s->resident].path, 64);
touch(s->resident);
return true;
}
// Serializing a whole contact table must not evict a dirty entry and
// recursively rewrite the table it is currently reading.
return _backend && s->location != NONE &&
_backend->readStoredPath(s->location, path) && checksum(path) == s->checksum;
}
const uint8_t* view(uint16_t h) override {
static const uint8_t empty[64] = {};
if (!h) return empty;
const int index = load(h);
return index < 0 ? nullptr : _entries[index].path;
}
bool replace(uint16_t& h, const uint8_t path[64]) override {
uint8_t previous[64];
if (read(h, previous) && memcmp(previous, path, 64) == 0) return true;
bool empty = true;
for (size_t i = 0; i < 64; ++i) empty = empty && path[i] == 0;
if (empty) { release(h); h = 0; return true; }
Slot* old = slot(h);
// In-place mutation is safe only when there is no ContactInfo snapshot.
if (old && old->refs == 1 && old->resident != 255) {
memcpy(_entries[old->resident].path, path, 64);
old->flags |= DIRTY;
old->checksum = checksum(path);
touch(old->resident);
return true;
}
const uint16_t next = allocate();
if (next == NONE) return false;
const int index = makeRoom();
if (index < 0) { release(next); return false; }
Slot& s = _slots[next - 1];
s.location = old ? old->location : NONE;
s.flags = DIRTY;
s.checksum = checksum(path);
s.resident = index;
memcpy(_entries[index].path, path, 64);
_entries[index].handle = next;
touch(index);
release(h);
h = next;
return true;
}
uint16_t source(uint16_t h) const override {
return h && h <= Handles && _slots[h - 1].refs ? _slots[h - 1].location : NONE;
}
void beginCommit() override {
_committing = true;
for (auto& s : _slots) s.flags &= ~(MARKED | DETACH);
}
void mark(uint16_t h) override { Slot* s = slot(h); if (s) s->flags |= MARKED; }
bool preserveSnapshots(uint16_t first, uint16_t count) override {
for (size_t i = 0; i < Handles; ++i) {
Slot& s = _slots[i];
if (!s.refs || s.location == NONE || (s.flags & MARKED) ||
s.location < first || static_cast<uint32_t>(s.location) >=
static_cast<uint32_t>(first) + count) continue;
if (load(i + 1) < 0) return false;
s.flags |= DETACH;
}
return true;
}
void publish(uint16_t h, uint16_t location) override {
Slot* s = slot(h);
if (s) { s->location = location; s->flags &= ~DIRTY; }
}
void endCommit(bool success) override {
for (auto& s : _slots) {
if (success && (s.flags & DETACH)) s.location = NONE;
s.flags &= ~(MARKED | DETACH);
}
_committing = false;
}
};
} // namespace mesh
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <Arduino.h>
#include "ContactCachePolicy.h"
#include <Identity.h>
#include <Utils.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
class ContactSecretBackend {
public:
virtual bool readSavedSecret(const uint8_t peer[32], const uint8_t identity[32],
uint8_t secret[32]) = 0;
virtual bool saveSecret(const uint8_t peer[32], const uint8_t identity[32],
const uint8_t secret[32]) = 0;
};
class ContactSecretCache {
struct Entry {
uint8_t peer[32] = {};
uint8_t secret[32] = {};
uint8_t age = 0;
bool valid = false;
} _entries[16];
uint8_t _identity[PRV_KEY_SIZE + PUB_KEY_SIZE] = {};
#if MESH_CONTACT_SECRET_FLASH_CACHE
uint8_t _identity_hash[32] = {};
#endif
bool _has_identity = false;
#if MESH_CONTACT_SECRET_FLASH_CACHE
ContactSecretBackend* _backend = nullptr;
#endif
public:
uint32_t ram_hits = 0;
uint32_t flash_hits = 0;
uint32_t calculations = 0;
uint32_t save_failures = 0;
uint32_t last_flash_read_us = 0;
uint32_t last_calculation_us = 0;
void attach(ContactSecretBackend* backend) {
#if MESH_CONTACT_SECRET_FLASH_CACHE
_backend = backend;
#else
(void)backend;
#endif
}
const uint8_t* get(const LocalIdentity& self, const uint8_t peer[32]) {
uint8_t identity[sizeof(_identity)];
self.writeTo(identity, sizeof(identity));
// Compare the complete key pair, including private material. An identity
// import must invalidate RAM and flash lookups even if its public bytes
// happen to be unchanged. Only a SHA-256 fingerprint is stored with a page.
if (!_has_identity || memcmp(identity, _identity, sizeof(identity)) != 0) {
for (auto& e : _entries) { memset(e.secret, 0, sizeof(e.secret)); e.valid = false; }
memcpy(_identity, identity, sizeof(identity));
#if MESH_CONTACT_SECRET_FLASH_CACHE
Utils::sha256(_identity_hash, sizeof(_identity_hash), identity, sizeof(identity));
#endif
_has_identity = true;
}
int chosen = -1;
bool hit = false;
for (int i = 0; i < 16; ++i) {
if (_entries[i].valid && memcmp(_entries[i].peer, peer, 32) == 0) {
chosen = i;
hit = true;
break;
}
if (chosen < 0 || !_entries[i].valid ||
(_entries[chosen].valid && _entries[i].age > _entries[chosen].age)) chosen = i;
}
Entry& e = _entries[chosen];
if (hit) {
++ram_hits;
} else {
e.valid = false;
memcpy(e.peer, peer, 32);
#if MESH_CONTACT_SECRET_FLASH_CACHE
const uint32_t started = micros();
if (_backend && _backend->readSavedSecret(peer, _identity_hash, e.secret)) {
last_flash_read_us = micros() - started;
++flash_hits;
} else
#endif
{
const uint32_t started = micros();
self.calcSharedSecret(e.secret, peer);
last_calculation_us = micros() - started;
++calculations;
#if MESH_CONTACT_SECRET_FLASH_CACHE
if (_backend && !_backend->saveSecret(peer, _identity_hash, e.secret)) ++save_failures;
#endif
}
e.valid = true;
}
for (auto& other : _entries) if (other.valid && other.age < 16) ++other.age;
e.age = 0;
return e.secret;
}
};
ContactSecretCache& contactSecretCache();
} // namespace mesh