Cache Full Companion paths and shared secrets to recover RAM
This commit is contained in:
1 parent
5013c8ff34
commit
6db10c3986
27 files changed
+2360
-58
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user