Skip to content

New Direct Mode - #3630

Draft
ripplebiz wants to merge 5 commits into
devfrom
new-direct-mode
Draft

ripplebiz wants to merge 5 commits into
devfrom
new-direct-mode

Conversation

@ripplebiz

Copy link
Copy Markdown
Member

This is an attempt to carve out a brand new Direct Mode by utilising the as-yet-unused TRANSPORT_DIRECT route type (0x03). ie. Packets with Direct + transport code data.

transport_code[0] is used as such:

  • bits 0..7: sequence number
  • bits 8..13: path cursor (0..63)
  • bits 14..15: DM_MODE_SEQ_* (sub-mode: one of NORMAL, WIDE, WIDE_MODIFIED, LINK_ID)

transport_code[1] is used as such:

  • bits 0..13: session-id
  • bits 14..15: crypto-mode (CRYPTO_LEGACY, CRYPTO_NEW_DIRECT, RESERVED1, RESERVED2)

Some of the things this new encoding/scheme is attempting is:

  • path is not consumed as it propagates along path. Dest node can see what the Full Path was. Instead, a simple 'cursor'/index is incremented at each hop, and path remains untouched (except in 'wide' mode).
  • 8-bit sequence numbers incremented by sender. Dest nodes should be able to do Link Quality calcs, ie. determine how many packets were lost.
  • A new 'wide' / 'fuzzy' sub-mode (DM_MODE_SEQ_WIDE).
  • Possibility for a number of different encryption methods. (a new placeholder one has a minimal impl for the CRYPTO_NEW_DIRECT slot, which attempts to use the seq-no/session-id fields as IV data)

NOTE: The 'wide' mode is inspired from @redxdev and this PR: #2648

Wide sub-mode is fairly simple, where (at each hop) the designated repeater with matching hash forwards, but also all direct neighbors of that repeater also forward. This could be a much more resilient direct mode. New clients could have a strategy, when sending, to use DM_MODE_SEQ_NORMAL (say first 2 attempts), then DM_MODE_SEQ_WIDE (for next attempt), then revert to FLOOD.

Also, if a neighbor repeater forwards it scribbles into the path its own hash, then edits the sub-mode to DM_MODE_SEQ_WIDE_MODIFIED. Dest node can then see if an alternate path was actually taken and use that info.

The _LINK_ID sub-mode is still effectively a reserved sub-mode, and hasn't been properly fleshed-out.

There are still a number of aspects that are yet to be done, like acting on the Link Quality calcs. (just placeholder updateLinkQualityStats() methods ATM)

@mikecarper

Copy link
Copy Markdown

Is this a use case for the blockchain path tracking that you were looking at? Allow one to mine the exact path taken using the discovery db of adverts. First 16 char is all you'd need to add to the blockchain

@ripplebiz

Copy link
Copy Markdown
Member Author

Is this a use case for the blockchain path tracking that you were looking at? Allow one to mine the exact path taken using the discovery db of adverts. First 16 char is all you'd need to add to the blockchain

No, this is unrelated.

@VoxMesh

VoxMesh commented Oct 19, 2026

Copy link
Copy Markdown

I think there's a case where WIDE can break an otherwise working route.

Suppose the path starts with A, B, .... Both A and B can hear the sender, and B has A in its neighbor table.

B can forward on behalf of A, replacing A with its own hash and advancing the cursor. This leaves [B, B, ...], with B still expected to handle the next hop.

However, B has already marked the packet as seen. When A forwards its copy, B will discard it as a duplicate, since calculatePacketHash() doesn't include the cursor. The packet can get stuck even though the original route was valid.

Instead of always advancing the cursor by one, could we check whether the forwarding repeater's own hash appears again later in the remaining path? If it does, we could advance the cursor past that occurrence and continue forwarding, effectively skipping the intermediate hops.

We'd also need to account for the skipped hops in the recorded path so it doesn't misrepresent the actual route.

Am I missing anything in the duplicate handling here?

@ripplebiz

Copy link
Copy Markdown
Member Author

I think there's a case where WIDE can break an otherwise working route.

Suppose the path starts with A, B, .... Both A and B can hear the sender, and B has A in its neighbor table.

B can forward on behalf of A, replacing A with its own hash and advancing the cursor. This leaves [B, B, ...], with B still expected to handle the next hop.

However, B has already marked the packet as seen. When A forwards its copy, B will discard it as a duplicate, since calculatePacketHash() doesn't include the cursor. The packet can get stuck even though the original route was valid.

Instead of always advancing the cursor by one, could we check whether the forwarding repeater's own hash appears again later in the remaining path? If it does, we could advance the cursor past that occurrence and continue forwarding, effectively skipping the intermediate hops.

We'd also need to account for the skipped hops in the recorded path so it doesn't misrepresent the actual route.

Am I missing anything in the duplicate handling here?

Yes, I think you're correct. That scenario would fail. Seems like removing duplicates of 'B' (in that example) before forwarding could be the fix, although it's a little sketchy if it's a 1-byte path hash. It could be conservative and only remove duplicate path hash (ahead of cursor) only if it's the next hop or the one after that(?) rather than scanning whole path.

@VoxMesh

VoxMesh commented Oct 20, 2026

Copy link
Copy Markdown

Yes, I think you're correct. That scenario would fail. Seems like removing duplicates of 'B' (in that example) before forwarding could be the fix, although it's a little sketchy if it's a 1-byte path hash. It could be conservative and only remove duplicate path hash (ahead of cursor) only if it's the next hop or the one after that(?) rather than scanning whole path.

Agreed, with 1-byte path hashes any fix seems like a trade-off.

I also considered keeping the repeated B in the path. When B substitutes for A in [A, B, C], giving [B, B, C], it could first forward with the cursor at C, then send a backup copy pointing at the second B.

That would cover both possibilities: the second B is the same repeater, or a different one with a matching prefix.

The downside is that duplicate handling would need to distinguish these forwarding stages, including on downstream repeaters. Plus the extra airtime.

Might be more complexity than this case warrants.

@redxdev

redxdev commented Oct 21, 2026 •

Copy link
Copy Markdown

I've been out of the loop lately so I'm glad to see the path cursor and wide mode are still being considered!

An additional thing that's worth considering is having isHashNeighbor return false if the neighbor has too low of an SNR (I think I required >= 0, but it being configurable probably makes sense) - I did that in my original proposal to try to limit wide paths to neighbors that would likely still be heard by the next hop. But maybe it's not necessary, haven't fully thought through it.

@ripplebiz

ripplebiz commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

I've been out of the loop lately so I'm glad to see the path cursor and wide mode are still being considered!

An additional thing that's worth considering is having isHashNeighbor return false if the neighbor has too low of an SNR (I think I required >= 0, but it being configurable probably makes sense) - I did that in my original proposal to try to limit wide paths to neighbors that would likely still be heard by the next hop. But maybe it's not necessary, haven't fully thought through it.

Good idea. It would be easy to add that as tuning config item, eg. "set wide.snr.thresh 1.0", but have it default to something like -8.0

est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
auto pkt = obtainPacketFor(recipient);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Both sendRequest() overloads shadow the outer pkt with auto pkt. The null check and encodeDatagram() use the inner pointer, but stdSend() gets the uninitialized outer one.

Looks like auto just needs to be dropped in both places.

// a DIRECT login can reply via the stored out_path, as onPeerDataRecv() does for REQ
ClientInfo* client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
bool have_out_path = client != NULL && client->out_path_len != OUT_PATH_UNKNOWN;
if (client == NULL) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This early return also drops responses to public REGIONS, OWNER and BASIC requests. Those handlers don't create an ACL entry, so a valid request from an unknown client gets no reply.

Could we preserve the reply path for these requests without requiring an ACL entry?

Comment thread src/Mesh.h
* \returns true, if path was accepted and that reciprocal path should be sent
*/
virtual bool onPeerPathRecv(Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { return false; }
virtual bool onPeerPathRecv(Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len, uint8_t crypto_type) { return false; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adding crypto_type here leaves SensorMesh::onPeerPathRecv() with the old 8-argument signature in both the header and implementation.

The override now fails to compile. Looks like the sensor override needs updating too. simple_sensor isn't covered by the current PR build matrix.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants