Phase 0.5 & 1: Centralize path utils, multi-hop packet decoding, updated PacketInfo shape

This commit is contained in:
Jack Kingsman
2026-03-07 18:34:47 -08:00
parent 0ac8e97ea2
commit 1fc041538e
4 changed files with 342 additions and 14 deletions

View File

@@ -79,9 +79,10 @@ class PacketInfo:
route_type: RouteType
payload_type: PayloadType
payload_version: int
path_length: int
path: bytes # The routing path (empty if path_length is 0)
path_length: int # Decoded hop count (not the raw wire byte)
path: bytes # The routing path bytes (empty if path_length is 0)
payload: bytes
path_hash_size: int = 1 # Bytes per hop: 1, 2, or 3
def calculate_channel_hash(channel_key: bytes) -> str:
@@ -100,12 +101,14 @@ def extract_payload(raw_packet: bytes) -> bytes | None:
Packet structure:
- Byte 0: header (route_type, payload_type, version)
- For TRANSPORT routes: bytes 1-4 are transport codes
- Next byte: path_length
- Next path_length bytes: path data
- Next byte: path byte (packed as [hash_mode:2][hop_count:6])
- Next hop_count * hash_size bytes: path data
- Remaining: payload
Returns the payload bytes, or None if packet is malformed.
"""
from app.path_utils import decode_path_byte, path_wire_len
if len(raw_packet) < 2:
return None
@@ -120,16 +123,17 @@ def extract_payload(raw_packet: bytes) -> bytes | None:
return None
offset += 4
# Get path length
# Decode packed path byte
if len(raw_packet) < offset + 1:
return None
path_length = raw_packet[offset]
hop_count, hash_size = decode_path_byte(raw_packet[offset])
offset += 1
# Skip path data
if len(raw_packet) < offset + path_length:
path_bytes = path_wire_len(hop_count, hash_size)
if len(raw_packet) < offset + path_bytes:
return None
offset += path_length
offset += path_bytes
# Rest is payload
return raw_packet[offset:]
@@ -139,6 +143,8 @@ def extract_payload(raw_packet: bytes) -> bytes | None:
def parse_packet(raw_packet: bytes) -> PacketInfo | None:
"""Parse a raw packet and extract basic info."""
from app.path_utils import decode_path_byte, path_wire_len
if len(raw_packet) < 2:
return None
@@ -156,17 +162,18 @@ def parse_packet(raw_packet: bytes) -> PacketInfo | None:
return None
offset += 4
# Get path length
# Decode packed path byte
if len(raw_packet) < offset + 1:
return None
path_length = raw_packet[offset]
hop_count, hash_size = decode_path_byte(raw_packet[offset])
offset += 1
# Extract path data
if len(raw_packet) < offset + path_length:
path_byte_len = path_wire_len(hop_count, hash_size)
if len(raw_packet) < offset + path_byte_len:
return None
path = raw_packet[offset : offset + path_length]
offset += path_length
path = raw_packet[offset : offset + path_byte_len]
offset += path_byte_len
# Rest is payload
payload = raw_packet[offset:]
@@ -175,7 +182,8 @@ def parse_packet(raw_packet: bytes) -> PacketInfo | None:
route_type=route_type,
payload_type=payload_type,
payload_version=payload_version,
path_length=path_length,
path_length=hop_count,
path_hash_size=hash_size,
path=path,
payload=payload,
)

70
app/path_utils.py Normal file
View File

@@ -0,0 +1,70 @@
"""
Centralized helpers for MeshCore multi-byte path encoding.
The path_len wire byte is packed as [hash_mode:2][hop_count:6]:
- hash_size = (hash_mode) + 1 → 1, 2, or 3 bytes per hop
- hop_count = lower 6 bits → 063 hops
- wire bytes = hop_count × hash_size
Mode 3 (hash_size=4) is reserved and rejected.
"""
def decode_path_byte(path_byte: int) -> tuple[int, int]:
"""Decode a packed path byte into (hop_count, hash_size).
Returns:
(hop_count, hash_size) where hash_size is 1, 2, or 3.
Raises:
ValueError: If hash_mode is 3 (reserved).
"""
hash_mode = (path_byte >> 6) & 0x03
if hash_mode == 3:
raise ValueError(f"Reserved path hash mode 3 (path_byte=0x{path_byte:02X})")
hop_count = path_byte & 0x3F
hash_size = hash_mode + 1
return hop_count, hash_size
def path_wire_len(hop_count: int, hash_size: int) -> int:
"""Wire byte length of path data."""
return hop_count * hash_size
def split_path_hex(path_hex: str, hop_count: int) -> list[str]:
"""Split a hex path string into per-hop chunks using the known hop count.
If hop_count is 0 or the hex length doesn't divide evenly, falls back
to 2-char (1-byte) chunks for backward compatibility.
"""
if not path_hex or hop_count <= 0:
return []
chars_per_hop = len(path_hex) // hop_count
if chars_per_hop < 2 or chars_per_hop % 2 != 0 or chars_per_hop * hop_count != len(path_hex):
# Inconsistent — fall back to legacy 2-char split
return [path_hex[i : i + 2] for i in range(0, len(path_hex), 2)]
return [path_hex[i : i + chars_per_hop] for i in range(0, len(path_hex), chars_per_hop)]
def first_hop_hex(path_hex: str, hop_count: int) -> str | None:
"""Extract the first hop identifier from a path hex string.
Returns None for empty/direct paths.
"""
hops = split_path_hex(path_hex, hop_count)
return hops[0] if hops else None
def infer_hash_size(path_hex: str, hop_count: int) -> int:
"""Derive bytes-per-hop from path hex length and hop count.
Returns 1 as default for ambiguous or legacy cases.
"""
if hop_count <= 0 or not path_hex:
return 1
hex_per_hop = len(path_hex) // hop_count
byte_per_hop = hex_per_hop // 2
if byte_per_hop in (1, 2, 3) and hex_per_hop * hop_count == len(path_hex):
return byte_per_hop
return 1