Files
meshcore-analyzer/cmd/server/hash_migrate_test.go
Kpa-clawbot a8e1cea683 fix: use payload type bits only in content hash (not full header byte) (#787)
## Problem

The firmware computes packet content hash as:

```
SHA256(payload_type_byte + [path_len for TRACE] + payload)
```

Where `payload_type_byte = (header >> 2) & 0x0F` — just the payload type
bits (2-5).

CoreScope was using the **full header byte** in its hash computation,
which includes route type bits (0-1) and version bits (6-7). This meant
the same logical packet produced different content hashes depending on
route type — breaking dedup and packet lookup.

**Firmware reference:** `Packet.cpp::calculatePacketHash()` uses
`getPayloadType()` which returns `(header >> PH_TYPE_SHIFT) &
PH_TYPE_MASK`.

## Fix

- Extract only payload type bits: `payloadType := (headerByte >> 2) &
0x0F`
- Include `path_len` byte in hash for TRACE packets (matching firmware
behavior)
- Applied to both `cmd/server/decoder.go` and `cmd/ingestor/decoder.go`

## Tests Added

- **Route type independence:** Same payload with FLOOD vs DIRECT route
types produces identical hash
- **TRACE path_len inclusion:** TRACE packets with different `path_len`
produce different hashes
- **Firmware compatibility:** Hash output matches manual computation of
firmware algorithm

## Migration Impact

Existing packets in the DB have content hashes computed with the old
(incorrect) formula. Options:

1. **Recompute hashes** via migration (recommended for clean state)
2. **Dual lookup** — check both old and new hash on queries (backward
compat)
3. **Accept the break** — old hashes become stale, new packets get
correct hashes

Recommend option 1 (migration) as a follow-up. The volume of affected
packets depends on how many distinct route types were seen for the same
logical packet.

Fixes #786

---------

Co-authored-by: you <you@example.com>
2026-04-18 11:52:22 -07:00

79 lines
2.0 KiB
Go

package main
import (
"testing"
"time"
)
func TestMigrateContentHashesAsync(t *testing.T) {
db := setupTestDBv2(t)
store := NewPacketStore(db, nil)
// Insert a packet with a manually wrong hash (simulating old formula).
rawHex := "0A00D69FD7A5A7475DB07337749AE61FA53A4788E976"
correctHash := ComputeContentHash(rawHex)
wrongHash := "deadbeef12345678"
_, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type)
VALUES (?, ?, datetime('now'), 0, 2)`, rawHex, wrongHash)
if err != nil {
t.Fatal(err)
}
if err := store.Load(); err != nil {
t.Fatal(err)
}
if store.byHash[wrongHash] == nil {
t.Fatal("expected packet under wrong hash before migration")
}
migrateContentHashesAsync(store, 100, time.Millisecond)
if !store.hashMigrationComplete.Load() {
t.Error("expected hashMigrationComplete to be true")
}
if store.byHash[wrongHash] != nil {
t.Error("old hash should be removed from index")
}
if store.byHash[correctHash] == nil {
t.Error("new hash should be in index")
}
var dbHash string
err = db.conn.QueryRow("SELECT hash FROM transmissions WHERE raw_hex = ?", rawHex).Scan(&dbHash)
if err != nil {
t.Fatal(err)
}
if dbHash != correctHash {
t.Errorf("DB hash = %s, want %s", dbHash, correctHash)
}
}
func TestMigrateContentHashesAsync_NoOp(t *testing.T) {
db := setupTestDBv2(t)
store := NewPacketStore(db, nil)
rawHex := "0A00D69FD7A5A7475DB07337749AE61FA53A4788E976"
correctHash := ComputeContentHash(rawHex)
_, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type)
VALUES (?, ?, datetime('now'), 0, 2)`, rawHex, correctHash)
if err != nil {
t.Fatal(err)
}
if err := store.Load(); err != nil {
t.Fatal(err)
}
migrateContentHashesAsync(store, 100, time.Millisecond)
if !store.hashMigrationComplete.Load() {
t.Error("expected hashMigrationComplete to be true")
}
if store.byHash[correctHash] == nil {
t.Error("hash should remain in index")
}
}