Files
beacon-server/internal/tracequality/quality.go

97 lines
2.6 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
// Package tracequality checks TRACE structure, not authenticity or RF plausibility.
package tracequality
import "encoding/hex"
type Quality struct {
Status string `json:"status"` // supported, suspect, or ambiguous; supported is not authenticated
Reasons []string `json:"reasons"`
}
func (q *Quality) Add(reason string) {
for _, r := range q.Reasons {
if r == reason {
return
}
}
q.Reasons = append(q.Reasons, reason)
if reason == "ambiguous_prefix" && q.Status != "suspect" {
q.Status = "ambiguous"
} else if reason != "ambiguous_prefix" {
q.Status = "suspect"
}
}
// Assess uses TRACE's power-of-two payload hash widths, NOT the ordinary
// packet path's 1/2/3-byte widths. The path field contains consumed-hop SNRs.
// -1 means a header/path field is unavailable in a retained summary.
// Mesh.cpp onRecvPacket/sendDirect: a trace uses direct routing, flags bits
// 0..1, and fewer than 64 SNR bytes. Partial probes and return loops are valid.
func Assess(raw []byte, version, route, pathLength int) Quality {
q := Quality{Status: "supported", Reasons: []string{}}
if version > 0 {
q.Add("unsupported_version")
}
if route >= 0 && route != 2 && route != 3 {
q.Add("non_direct_trace")
}
if len(raw) < 9 {
q.Add("incomplete_payload")
return q
}
if len(raw) > 184 {
q.Add("payload_too_large")
}
if raw[8]&0xfc != 0 {
q.Add("unsupported_flags")
}
size := 1 << (raw[8] & 3)
if (len(raw)-9)%size != 0 {
q.Add("misaligned_hashes")
}
count := (len(raw) - 9) / size
if count > 63 {
q.Add("too_many_hops")
}
if pathLength > 63 || pathLength > count {
q.Add("invalid_snr_path")
}
return q
}
// Stored also checks old parsed rows, without rewriting or deleting the evidence.
func Stored(raw string, flags byte, hashes []string, snrs []float32) Quality {
b, err := hex.DecodeString(raw)
if err != nil {
return Quality{Status: "suspect", Reasons: []string{"incomplete_payload"}}
}
if raw == "" { // Older exports may contain only decoded fields.
b = make([]byte, 9)
b[8] = flags
for _, h := range hashes {
v, e := hex.DecodeString(h)
if e != nil {
return Quality{Status: "suspect", Reasons: []string{"misaligned_hashes"}}
}
b = append(b, v...)
}
}
q := Assess(b, -1, -1, len(snrs))
size := 1 << (flags & 3)
if len(b) >= 9 && b[8] != flags {
q.Add("misaligned_hashes")
}
for _, h := range hashes {
if len(h) != 2*size {
q.Add("misaligned_hashes")
}
}
if len(b) >= 9 && len(hashes) != (len(b)-9)/size {
q.Add("misaligned_hashes")
}
return q
}