fix: parse TRACE packet path hops from payload instead of header (#277)

Fixes #276

## Root cause

TRACE packets store hop IDs in the payload (bytes 9+) rather than in the
header path field. The header path field is overloaded in TRACE packets
to carry RSSI values instead of repeater IDs (as noted in the issue
comments). This meant `Path.Hops` was always empty for TRACE packets —
the raw bytes ended up as an opaque `PathData` hex string with no
structure.

The hashSize encoded in the header path byte (bits 6–7) is still valid
for TRACE and is used to split the payload path bytes into individual
hop prefixes.

## Fix

After decoding a TRACE payload, if `PathData` is non-empty, parse it
into individual hops using `path.HashSize`:

```go
if header.PayloadType == PayloadTRACE && payload.PathData != "" {
    pathBytes, err := hex.DecodeString(payload.PathData)
    if err == nil && path.HashSize > 0 {
        for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize {
            path.Hops = append(path.Hops, ...)
        }
    }
}
```

Applied to both `cmd/ingestor/decoder.go` and `cmd/server/decoder.go`.

## Verification

Packet from the issue: `260001807dca00000000007d547d`

| | Before | After |
|---|---|---|
| `Path.Hops` | `[]` | `["7D", "54", "7D"]` |
| `Path.HashCount` | `0` | `3` |

New test `TestDecodeTracePathParsing` covers this exact packet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
efitenandClaude Sonnet 4.6 authored and GitHub committed 2026-03-30 16:27:50 +00:00
1 parent 726b041740
commit 8f833f64ae
3 files changed
+55

No files matched your search

+15
View File
@@ -572,6 +572,21 @@ func DecodePacket(hexString string, channelKeys map[string]string) (*DecodedPack
payloadBuf := buf[offset:]
payload := decodePayload(header.PayloadType, payloadBuf, channelKeys)
// TRACE packets store hop IDs in the payload (buf[9:]) rather than the header
// path field. The header path byte still encodes hashSize in bits 6-7, which
// we use to split the payload path data into individual hop prefixes.
if header.PayloadType == PayloadTRACE && payload.PathData != "" {
pathBytes, err := hex.DecodeString(payload.PathData)
if err == nil && path.HashSize > 0 {
hops := make([]string, 0, len(pathBytes)/path.HashSize)
for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize {
hops = append(hops, strings.ToUpper(hex.EncodeToString(pathBytes[i:i+path.HashSize])))
}
path.Hops = hops
path.HashCount = len(hops)
}
}
return &DecodedPacket{
Header: header,
TransportCodes: tc,
+25
View File
@@ -564,6 +564,31 @@ func TestDecodeTraceValid(t *testing.T) {
}
}
func TestDecodeTracePathParsing(t *testing.T) {
// Packet from issue #276: 260001807dca00000000007d547d
// Path byte 0x00 → hashSize=1, hops in payload at buf[9:] = 7d 54 7d
// Expected path: ["7D", "54", "7D"]
pkt, err := DecodePacket("260001807dca00000000007d547d", nil)
if err != nil {
t.Fatalf("DecodePacket error: %v", err)
}
if pkt.Payload.Type != "TRACE" {
t.Errorf("payload type=%s, want TRACE", pkt.Payload.Type)
}
want := []string{"7D", "54", "7D"}
if len(pkt.Path.Hops) != len(want) {
t.Fatalf("hops=%v, want %v", pkt.Path.Hops, want)
}
for i, h := range want {
if pkt.Path.Hops[i] != h {
t.Errorf("hops[%d]=%s, want %s", i, pkt.Path.Hops[i], h)
}
}
if pkt.Path.HashCount != 3 {
t.Errorf("hashCount=%d, want 3", pkt.Path.HashCount)
}
}
func TestDecodeAdvertShort(t *testing.T) {
p := decodeAdvert(make([]byte, 50))
if p.Error != "too short for advert" {
+15
View File
@@ -373,6 +373,21 @@ func DecodePacket(hexString string) (*DecodedPacket, error) {
payloadBuf := buf[offset:]
payload := decodePayload(header.PayloadType, payloadBuf)
// TRACE packets store hop IDs in the payload (buf[9:]) rather than the header
// path field. The header path byte still encodes hashSize in bits 6-7, which
// we use to split the payload path data into individual hop prefixes.
if header.PayloadType == PayloadTRACE && payload.PathData != "" {
pathBytes, err := hex.DecodeString(payload.PathData)
if err == nil && path.HashSize > 0 {
hops := make([]string, 0, len(pathBytes)/path.HashSize)
for i := 0; i+path.HashSize <= len(pathBytes); i += path.HashSize {
hops = append(hops, strings.ToUpper(hex.EncodeToString(pathBytes[i:i+path.HashSize])))
}
path.Hops = hops
path.HashCount = len(hops)
}
}
return &DecodedPacket{
Header: header,
TransportCodes: tc,