diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index f40dc827..a0885c47 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -168,6 +168,62 @@ uint64_t monotonicMillis() { return static_cast(esp_timer_get_time() / 1000LL); } +// --------------------------------------------------------------------------- +// App-only diagnostic: on-demand identity acquisition for unknown LXMF +// sources (mirrors Sideband's "Query Network For Keys" button). +// +// When an LXMF message arrives from a source whose RNS identity has not been +// learned, the router accepts the message but UIManager::on_message_received +// skips location ingest for unauthenticated senders. Nothing in the firmware +// ever triggered the identity learning that microLXMF's comment at +// LXMRouter.cpp:1267 expects ("signature will be validated later if the +// source identity is learned via announce"). This fires a bounded +// RNS path request — the exact mechanism of Sideband's +// RNS.Transport.request_path — so any peer that already knows the source's +// key (the hub, a phone, or another node) can answer with the cached +// announce. The next message from that peer then validates and plots. +// +// Rate-limited per source hash (5 min) and globally (64 entries) to keep +// opportunistic-flood cost to zero in the steady state. Diagnostic for the +// missing-map-pin investigation; remove with the other ingest diagnostics +// once the root cause is closed. +namespace { +struct KeyRequestState { + std::vector> last_requested; +}; +KeyRequestState s_key_request_state; + +void request_key_for_unknown_source(const ::LXMF::LXMessage& message) { + if (message.unverified_reason() != + ::LXMF::Type::Message::SOURCE_UNKNOWN) { + return; // Bad signature is not recoverable by asking the network. + } + const RNS::Bytes& source_hash = message.source_hash(); + if (source_hash.size() != Telemetry::PEER_ID_SIZE) { + return; + } + const std::string source_hex = source_hash.toHex(); + const uint64_t now = monotonicMillis(); + const static constexpr uint64_t kKeyRequestIntervalMillis = 5 * 60 * 1000; + for (auto& e : s_key_request_state.last_requested) { + if (e.first == source_hex) { + if (now < e.second + kKeyRequestIntervalMillis) { + return; // Recently requested — wait for the answer to land. + } + e.second = now; // Expired — re-request below. + break; + } + } + if (s_key_request_state.last_requested.size() >= 64U) { + s_key_request_state.last_requested.erase(s_key_request_state.last_requested.begin()); + } + s_key_request_state.last_requested.emplace_back(source_hex, now); + INFO(("Requesting network keys for unknown LXMF source " + + source_hex.substr(0, 8) + "...").c_str()); + Transport::request_path(source_hash); +} +} // namespace + bool peerIdFromHash(const Bytes& hash, Telemetry::PeerId& output) { if (hash.size() != Telemetry::PEER_ID_SIZE) return false; std::memcpy(output.bytes, hash.data(), Telemetry::PEER_ID_SIZE); @@ -1585,6 +1641,14 @@ void UIManager::on_message_received(::LXMF::LXMessage& message) { pyxis_test_hook_record_rx(message); #endif + // Diagnostic: unauthenticated messages are accepted by the router but + // skipped below for location ingest. Ask the network for the source's + // key (Sideband "request keys" equivalent) so the NEXT message from + // this peer can validate and be plotted. + if (!message.signature_validated()) { + request_key_for_unknown_source(message); + } + // Classify authenticated location fields before any conversation write. // Raw field keys and values are MessagePack spans retained by LXMessage. Telemetry::InboundLocationMessage inbound{};