feat(meshcore): 2/3-byte path hash support in Define Path editor (#3670)#3686
Conversation
MeshCore packs the per-hop routing-hash width into the top 2 bits of the
path_len byte (hash_size-1) for BOTH OTA packets and contact records, and
firmware routes a contact's out_path verbatim through that packed format
(verified against ripplebiz/MeshCore: Packet.h/.cpp, ContactInfo.h,
Mesh::sendDirect). 1-byte is the default and packed==plain there, which is
why only 1-byte worked before. This adds authoring of 2/3-byte paths.
Backend:
- meshcoreNativeBackend set_out_path: accept hash_bytes (1/2/3). For 1-byte
keep meshcore.js's proven setContactPath; for 2/3-byte bypass it (it only
ever writes a plain byte count) and call addOrUpdateContact directly with
a hand-packed out_path_len = ((hash_bytes-1)<<6)|hop_count.
- meshcoreManager.setContactOutPath(publicKey, bytes, hashBytes): forward
hash_bytes; group the mirrored outPath string into width-wide hop tokens
('a3f2,7f01') and store pathLen as hop COUNT not byte count.
- parseHexPathChain(input, hashBytes): width-aware token validation/parsing.
- PUT out-path: validate hashBytes (1/2/3) and hop count (<=63).
- traceContactPath: expand multi-byte hop tokens to constituent bytes
instead of truncating each token to one byte via parseInt.
No DB migration: width is carried in the out_path hex string itself.
Frontend:
- meshcorePath utils threaded with hashBytes; new pathHashBytesOf() infers
width from an existing path.
- Define-Path editor: 1/2/3-byte hop-hash-width selector (pre-selected from
the current path), width-aware hop add/validation, sends hashBytes in PUT.
Tests: path-utils multi-byte, native-backend packed-byte assertion (0x42),
route multi-byte + hashBytes validation, component selector + width
inference. Full suite: 7347 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBLhGGNh35oMwTL53va1Y5
|
Claude finished @Yeraze's task —— View job PR Review: feat(meshcore): 2/3-byte path hash supportTasks:
Code ReviewThis is a well-implemented feature with solid protocol verification, good test coverage, and clean layering. Here are my observations: Strengths
Issues Found1. const n = parseInt(tok.slice(j * 2, j * 2 + 2), 16);
if (!Number.isFinite(n) || n < 0 || n > 0xff) return null;The 2. Double // In resolvePublicKey():
const contacts: any[] = await this.connection.getContacts();
// Then immediately after:
const contacts: any[] = await c.getContacts(); // Line 1003
3. const pathBytes = Uint8Array.from(
contact.outPath.split(',').flatMap((h) => {
const tok = h.trim();
const bytes: number[] = [];
for (let i = 0; i + 2 <= tok.length; i += 2) {
bytes.push(parseInt(tok.slice(i, i + 2), 16));
}
return bytes;
}),
);This correctly expands 4. const hopOptions = React.useMemo(
() => repeaterHopOptions(repeaters ?? [], editorHashBytes),
[repeaters, editorHashBytes],
);This is correctly memoized. ✅ 5. .filter((s) => /^[0-9a-f]+$/.test(s) && s.length % 2 === 0 && s.length >= 2 && s.length <= 6);This filters out bad tokens rather than returning an error. This is fine for display/parsing of stored paths (where the caller just wants the valid hops), but it means a malformed stored path is displayed silently truncated. The behavior is consistent with existing code, just worth being aware of. 6. UI width selector clears hops even on same-width selection ( const changeHashBytes = (next: 1 | 2 | 3) => {
if (next === editorHashBytes) return; // ← early exit on same value ✅
setEditorHashBytes(next);
setEditorHops([]);
...
};Actually handles this correctly — the early return prevents clearing when the user clicks the already-selected option. ✅ 7. Missing test: Tests cover 1-byte ( 8. Route error message uses hardcoded example ( error: `Invalid outPath — expected a comma-separated hex chain of ${hashBytes}-byte hops (${hashBytes * 2} hex chars each), e.g. "${'a3f27f01'.slice(0, hashBytes * 2)}"`,
SummaryThe core logic is correct and safe. The protocol-level packed byte math is right, backward compatibility is preserved, and the test suite covers the critical dispatch paths. The issues above are minor — a missing 3-byte test case is the most meaningful gap. The deferred No blocking issues. The feature is ready to merge pending any nits the team wants to address. |
Summary
Closes #3670 — lets users author MeshCore forwarding paths with 2- and 3-byte per-hop routing hashes, not just the 1-byte default, in Direct Messages → Contact Details → Define Path.
Protocol verification
Before writing any device-facing code I traced this through the MeshCore firmware (
ripplebiz/MeshCore,main) because the write path was ambiguous:path_lenis a single packed byte — top 2 bits =hash_size − 1(1/2/3-byte hops; 4-byte rejected byisValidPathLen), bottom 6 bits = hop count. (Packet.hgetPathHashSize/Count,setPathHashSizeAndCount.)ContactInfo.out_path_lenis stored raw andMesh::sendDirectcopiescontact.out_path/out_path_lenverbatim into the routed packet viacopyPath. So a packed contact length round-trips and routes correctly.setContactPath()hardcodesoutPathLen = path.length(plain) — correct only for 1-byte. For 2/3-byte we must bypass it.Backend
meshcoreNativeBackendset_out_path: acceptshash_bytes(1/2/3). 1-byte keeps the provensetContactPath; 2/3-byte bypasses it and callsaddOrUpdateContactdirectly with a hand-packedout_path_len = ((hash_bytes-1)<<6)|hop_countand the flat hop buffer.meshcoreManager.setContactOutPath(pk, bytes, hashBytes): forwardshash_bytes; the optimistic mirror now groups the byte buffer into width-wide hop tokens ("a3f2,7f01") and storespathLenas hop count.parseHexPathChain(input, hashBytes): each token must be exactlyhashBytes*2hex chars./contacts/:pk/out-path: validateshashBytes∈ {1,2,3} and hop count ≤ 63.traceContactPath: expands multi-byte hop tokens into their constituent bytes instead of truncating each token to one byte (silent corruption now that multi-byte paths can exist).No DB migration — the per-hop width is encoded in the stored
out_pathhex string (token length), so it round-trips without a schema change.Frontend
meshcorePath.tsutils are threaded withhashBytes; newpathHashBytesOf()infers width from an existing path.hashBytesin the PUT. Changing width clears the (width-specific) hop list.Testing
0x42for a 2-byte/2-hop path viaaddOrUpdateContact, and that 1-byte still usessetContactPath); route multi-byte forwarding +hashBytes/width validation; component width-selector + width-inference tests.Follow-up (not in this PR)
Full multi-byte support for the trace-path diagnostic at the bridge/firmware layer (this PR only fixes the byte extraction so it no longer truncates).
🤖 Generated with Claude Code