Skip to content

fix : avoid our node to be a relaynode candidate#4037

Merged
jamesarich merged 3 commits into
meshtastic:mainfrom
emp3r0r7:fix/relaynode-avoid-ournode
Dec 18, 2025
Merged

fix : avoid our node to be a relaynode candidate#4037
jamesarich merged 3 commits into
meshtastic:mainfrom
emp3r0r7:fix/relaynode-avoid-ournode

Conversation

@emp3r0r7

@emp3r0r7 emp3r0r7 commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

What

Improves relay node resolution by preventing the local node and unseen nodes
(lastHeard == 0) from being selected as relay candidates.

Why

Suffix-based node ID matching may result in false positives, causing the local
node or placeholder nodes to be incorrectly resolved as relay nodes. This can
lead to inaccurate relay info and misleading debug information.

How

  • Filtered out nodes that have never been heard from
  • Explicitly excluded the local node from relay candidate resolution

Fixes #4036

@github-actions github-actions Bot added the bugfix PR tag label Dec 18, 2025
@codecov

codecov Bot commented Dec 18, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.56%. Comparing base (59a0ad6) to head (96425ff).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...g/meshtastic/feature/messaging/MessageListPaged.kt 0.00% 4 Missing ⚠️
...astic/feature/settings/debugging/DebugViewModel.kt 0.00% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##            main   #4037      +/-   ##
========================================
- Coverage   0.56%   0.56%   -0.01%     
========================================
  Files        405     405              
  Lines      24163   24167       +4     
  Branches    3100    3102       +2     
========================================
  Hits         136     136              
- Misses     24006   24010       +4     
  Partials      21      21              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes relay node resolution by filtering out unseen nodes (lastHeard == 0) and explicitly excluding the local node from being selected as a relay candidate. This prevents false positives from suffix-based node ID matching that could result in inaccurate relay information.

Key Changes:

  • Added ourNodeNum parameter to Packet.getRelayNode() to enable local node exclusion
  • Filtered out nodes with lastHeard == 0 to exclude placeholder/unseen nodes
  • Updated all call sites to pass the local node number

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt Modified getRelayNode() to accept ourNodeNum parameter, filter unseen nodes, and exclude local node from relay candidates
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt Updated to retrieve and pass myNodeNum to getRelayNode() for relay node annotation
feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt Added ourNode parameter to MessageStatusDialog and passed ourNode?.num to getRelayNode()

Comment on lines +88 to +101
val candidateRelayNodes =
nodes.filter { it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix }

val closestRelayNode =
if (candidateRelayNodes.size == 1) {
candidateRelayNodes.first()
} else {
candidateRelayNodes.minByOrNull { it.hopsAway }
}

if (closestRelayNode?.num == ourNodeNum) {
// it's us, useless to return relay node for ourselves
return null
}

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local node exclusion logic should be moved into the filter predicate (line 89) rather than being checked after finding the closest relay node. The current implementation has a bug: if there are multiple candidate relay nodes and the local node happens to be the closest (lowest hopsAway), it will return null instead of returning the second-closest relay node.

The filter should be:

nodes.filter { it.num != ourNodeNum && it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix }

This ensures the local node is excluded upfront and allows the algorithm to correctly select the next closest relay node when needed.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to 104
fun getRelayNode(relayNodeId: Int, nodes: List<Node>, ourNodeNum: Int?): Node? {
val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK
val candidateRelayNodes = nodes.filter { (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix }

val candidateRelayNodes =
nodes.filter { it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix }

val closestRelayNode =
if (candidateRelayNodes.size == 1) {
candidateRelayNodes.first()
} else {
candidateRelayNodes.minByOrNull { it.hopsAway }
}

if (closestRelayNode?.num == ourNodeNum) {
// it's us, useless to return relay node for ourselves
return null
}

return closestRelayNode
}

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new relay node filtering logic lacks test coverage. Since the database module has existing unit tests (DatabaseManagerEvictionTest), this critical bug fix should include unit tests for the getRelayNode function to verify:

  1. Nodes with lastHeard == 0 are excluded
  2. The local node (matching ourNodeNum) is excluded
  3. Correct relay node selection when multiple candidates exist
  4. Proper handling when ourNodeNum is null
  5. Edge case: when the only candidate is the local node

Copilot uses AI. Check for mistakes.
Comment on lines 449 to 456
val relayNodeName by
remember(message.relayNode, nodes) {
derivedStateOf {
message.relayNode?.let { relayNodeId -> Packet.getRelayNode(relayNodeId, nodes)?.user?.longName }
message.relayNode?.let { relayNodeId ->
Packet.getRelayNode(relayNodeId, nodes, ourNode?.num)?.user?.longName
}
}
}

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remember block is missing ourNode in its dependency list. The derivedStateOf uses ourNode?.num on line 453, but ourNode is not included in the remember keys on line 450. This means if ourNode changes (e.g., from null to a valid node), the relayNodeName won't be recomputed, potentially causing stale relay node information to be displayed.

Copilot uses AI. Check for mistakes.
@jamesarich

jamesarich commented Dec 18, 2025

Copy link
Copy Markdown
Collaborator

I'm a bit hesitant to move forward with this feature at all - it was a bit half-baked to begin with and has had conversation about reworking. The partial id matching is pretty bad especially in larger networks where more collisions may occur.

@RCGV1 has been involved in a lot of those conversations and may have more insight on the future of this feature.

@emp3r0r7

emp3r0r7 commented Dec 18, 2025

Copy link
Copy Markdown
Contributor Author

I'm a bit hesitant to move forward with this feature at all - it was a bit half-baked to begin with and has had conversation about reworking. The partial id matching is pretty bad especially in larger networks where more collisions may occur.

@RCGV1 has been involved in a lot of those conversations and may have more insight on the future of this feature.

Hi, thanks for the feedback.

In my independent development, I’m working on a UI feature that displays the
currently resolved relay node available . While doing so,
I noticed that the local node is sometimes selected as a relay candidate, which
is what prompted this PR.

I agree that relay resolution may still fail in very large or dense networks by using the method getRelayNode.
To mitigate this in my own experiments, I also set the relay node to the most recently detected
direct node, or to the first responding node from a successful traceroute , i dont know if this could be a good idea for the future development of this feature, i personally find it very useful and in my own implementation, combining these

3 methods :
relay_node from packet OR
0 hops from a packet node different from us OR
first responding node in successful traceroute request

Works decently.

During testing, filtering out nodes obtained by getRelayNode with lastHeard != 0 and explicitly
excluding the local node consistently improved the accuracy of nearest relay
detection, without introducing regressions.

Also, copilot is right, im sorry i should have filtered before checking , i'll fix it

@jamesarich

Copy link
Copy Markdown
Collaborator

Let's give it a try - the current implementation is bad, and this looks like it'll improve things.

Thanks!

@jamesarich jamesarich added this pull request to the merge queue Dec 18, 2025
Merged via the queue into meshtastic:main with commit d8a1f88 Dec 18, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Relay node resolution may select local or unseen nodes

3 participants