fix(meshtastic): only push add_contact when the radio lacks the peer's key (#4368) - #4369
Conversation
…s key (#4368) MeshMonitor sent an `add_contact` admin message before EVERY PKI-encrypted DM, with no check for whether the radio already had that contact. That is not a free no-op: the firmware's `NodeDB::addFromContact()` sets `is_favorite = true` to shield the entry from NodeDB eviction, so every DM — including every automated Auto-Ack reply — re-favorited its recipient. The favorite then came back through the NodeInfo downsync, which made it look like foreign state was leaking in when MeshMonitor had caused it. Adds `deviceContactKeyNums`: nodes we have positive evidence the radio already holds a public key for. Deliberately distinct from `deviceNodeNums`, which only means the radio knows the node exists — it can know a node without holding its key, and guarding on that alone would break PKI DMs in exactly that case. Evidence is recorded when: - the device's own NodeDB dump reports user.publicKey (most authoritative — the radio telling us what it holds) - a mesh NodeInfo carrying a key reaches us THROUGH the radio, so the radio saw and stored the same key - we successfully push a contact ourselves and invalidated when: - the connection resets (a different radio, or one whose NodeDB was wiped, may not hold the same keys) - a key repair purges the node via removeDeviceNodeNum — the purge takes the key with it, so the next DM must push again Net effect: the radio still gets the key whenever it genuinely lacks one, so PKI DMs keep working, but a node is no longer re-favorited on every message. Tests drive the real sendTextMessage and the real NodeInfo-processing methods rather than re-implementing the predicate. Verified non-vacuous: with the guard disabled, 3 of the 9 fail, including "does NOT re-push on subsequent DMs". Note: no CLIENT_BASE special-casing here. Current firmware already implements the meshtastic/design#39 exception in both addFromContact and AdminModule's admin-key auto-favorite, so that alignment belongs at the firmware layer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9NzRtqE8eSMS8tvAeodUB
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshtastic): only push add_contact when the radio lacks the peer's keyTasks:
Overall AssessmentThis is a well-reasoned, carefully scoped fix with good documentation and solid test coverage. The approach is correct: tracking key evidence separately from node presence is the right design. Below are observations across quality, correctness, and coverage. Implementation (
|
Two changes landed around the release PR and are not in its notes: #4366 merged into main while #4367 sat in a stuck GitHub merge lock, and #4369 is queued to merge before the tag. Both ship in 4.13.2, so both need entries. #4366 — MeshCore Virtual Node "Allow PKI export" gate. Adds the CHANGELOG entry, a blog item, and the feature docs it shipped without: a new "PKI private-key export (MeshCore)" section in docs/configuration/virtual-node.md covering what the gate does and the three things to know before enabling it — the VN port has no client authentication, the private key is a strictly larger grant than admin commands (change the node vs. become it, which is why it is a separate switch), and gate-off replies Disabled, byte-identical to firmware built without ENABLE_PRIVATE_KEY_EXPORT. #4368 — add_contact pushed before every PKI DM re-favorited the recipient, because the firmware's addFromContact() sets is_favorite to shield the entry from NodeDB eviction. Worth more than a one-line fix entry: it explains a symptom users saw and misattributed to the auto-favorite feature, so the blog gets its own short section on it. Claude-Session: https://claude.ai/code/session_01PZtasD4tS76xq2PTDHMA5o Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #4368.
Problem
MeshMonitor sent an
add_contactadmin message before every PKI-encrypted DM, with no check for whether the radio already had that contact (meshtasticManager.ts, thepushContactToRadiocall insendTextMessage).That is not a free no-op. The firmware's
NodeDB::addFromContact()setsis_favorite = trueto shield the entry from NodeDB eviction:So every DM — including every automated Auto-Ack reply — re-favorited its recipient. The favorite then came back through the NodeInfo downsync, which made it look like foreign device state was leaking in when MeshMonitor had caused it.
This matches the reported symptom exactly: every DM recipient favorited, regardless of hop count or role. The zero-cost-hop auto-favorite feature (0-hop + relay-role gated) could never explain that.
Fix
New
deviceContactKeyNumsset: nodes we have positive evidence the radio already holds a public key for.Deliberately distinct from
deviceNodeNums, which only means the radio knows the node exists — it can know a node without holding its key, and guarding on that alone would silently break PKI DMs in exactly that case.Evidence recorded when:
user.publicKey— most authoritative, the radio stating what it holdsEvidence invalidated when:
removeDeviceNodeNum— the purge takes the key with it, so the next DM must push againNet effect: the radio still gets the key whenever it genuinely lacks one, so PKI DMs keep working — but a node is no longer re-favorited on every message.
What this deliberately does NOT do
isFavoritedownsync. An earlier revision of [BUG] add_contact is re-sent before every PKI DM, so the firmware re-favorites every DM recipient #4368 proposed that. It would have left MeshMonitor favoriting nodes on the device while hiding the fact from the UI, and regressed [BUG] Favorites are not updated after reconnect to node #213 (offline favorite changes). The downsync is correct and untouched.addFromContactandAdminModule.cpp:110(admin-key auto-favorite). That alignment belongs at the firmware layer, and duplicating it here would be redundant.Testing
9 new tests in
meshtasticManager.contactPushGuard.test.ts, driving the realsendTextMessageand the real NodeInfo-processing methods rather than re-implementing the predicate (the pattern the existingpushContactToRadio guardblock inmeshtasticManager.test.tsuses).Verified non-vacuous — with the guard reverted, 3 of the 9 fail:
does NOT re-push on subsequent DMs to the same nodenever pushes when the radio already reported the key from its own NodeDBnever pushes when a mesh NodeInfo carrying the key reached us through the radioAlso covered: first-DM still pushes, re-push after key-repair purge, per-connection reset, per-node isolation, and the two no-push paths (key mismatch, no public key).
tsc --noEmitclean;lint:ciratchet clean.Full local suite: 11,112 passed, 0 failed (679 skipped = the PostgreSQL/MySQL suites; no schema or migration work in this PR).
🤖 Generated with Claude Code
https://claude.ai/code/session_01L9NzRtqE8eSMS8tvAeodUB