Files
meshcore-analyzer/cmd/server/obs_dedup_test.go
T
n30nex abad8a26b3 fix(liveness): require evidence from each observed relay frame (#2298)
Red commit: `46e3b082` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203680455)).

Fixes #2266 and #2267. Certain relay activity now requires an eligible
path from the observation that supplied it. A representative
transmission header, destination attribution or catalogue uniqueness
alone cannot supply that evidence. Later valid FLOOD evidence survives
observer/path deduplication.

The fix preserves valid own packets and excludes failed signatures,
planned DIRECT/TRACE paths, ambiguous/retired prefixes and missing or
mismatched headers. Firmware reference: meshcore-dev/MeshCore
`81dbae59`.

- One eligibility byte replaces an unused raw-frame field. StoreObs
remains 152 bytes in the protected 160-byte allocation class; no wire
frames are retained. Existing dedup maps gain one pointer per slot only
at 64+ observations.
- Processing is linear in retained paths, bounded by 64 wire path bytes.
No new SQL, schema, configuration or maps.
- Tests cover five load/ingest paths, late duplicates,
analytics/adaptive thresholds, positive controls and concurrent readers.
Review correction `4ad6c5ad` preserves lazy decoded caches while
retaining escaped/legacy signature rejection.
- Full server suite, focused/race checks and frontend checks pass.
Browser verified on http://127.0.0.1:18905: both upstream false-positive
cases disappear and a genuine later FLOOD is recognized.

Independent review findings are addressed. Local preflight checks
passed; external home-directory bundle unavailable. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40205015381)
passed on `4ad6c5ad`, including all three browser shards and both
architecture builds.
2026-10-26 15:42:01 +01:00

135 lines
4.6 KiB
Go

package main
import (
"fmt"
"testing"
)
func addTestObs(tx *StoreTx, observerID, pathJSON string) bool {
if tx.hasObservation(observerID, pathJSON) {
return false
}
tx.addObservation(&StoreObs{ObserverID: observerID, PathJSON: pathJSON})
return true
}
// TestObsDedupCorrectness: the same observer with the same path is a
// duplicate; a new path or a new observer is not.
func TestObsDedupCorrectness(t *testing.T) {
tx := &StoreTx{ID: 1, Hash: "abc123"}
for i := 0; i < 5; i++ {
if !addTestObs(tx, fmt.Sprintf("obs-%d", i), fmt.Sprintf(`["path-%d"]`, i)) {
t.Fatalf("observation %d should not be a duplicate", i)
}
}
if tx.ObservationCount != 5 || len(tx.Observations) != 5 {
t.Fatalf("expected 5 observations, got count=%d len=%d", tx.ObservationCount, len(tx.Observations))
}
for i := 0; i < 5; i++ {
if !tx.hasObservation(fmt.Sprintf("obs-%d", i), fmt.Sprintf(`["path-%d"]`, i)) {
t.Fatalf("observation %d should be detected as duplicate", i)
}
}
if tx.hasObservation("obs-0", `["different-path"]`) {
t.Fatal("different path should not be a duplicate")
}
if tx.hasObservation("obs-new", `["path-0"]`) {
t.Fatal("different observer should not be a duplicate")
}
}
// TestObsDedupSmallTxCarriesNoMaps: most transmissions have a handful of
// observations, and two maps each cost them a few hundred bytes. Below the
// threshold the checks scan tx.Observations instead.
func TestObsDedupSmallTxCarriesNoMaps(t *testing.T) {
tx := &StoreTx{ID: 1}
for i := 0; i < obsIndexThreshold-1; i++ {
addTestObs(tx, fmt.Sprintf("obs-%d", i%4), fmt.Sprintf(`["p%d"]`, i))
}
if len(tx.Observations) != obsIndexThreshold-1 {
t.Fatalf("len = %d, want %d", len(tx.Observations), obsIndexThreshold-1)
}
if tx.obsKeys != nil || tx.observerSet != nil {
t.Fatal("a transmission under the threshold should not carry dedup maps")
}
if tx.UniqueObserverCount != 4 {
t.Fatalf("UniqueObserverCount = %d, want 4", tx.UniqueObserverCount)
}
}
// TestObsDedupAcrossTheThreshold: past the threshold the maps take over and
// give the same answers as the scan did.
func TestObsDedupAcrossTheThreshold(t *testing.T) {
tx := &StoreTx{ID: 1}
n := obsIndexThreshold * 3
for i := 0; i < n; i++ {
if !addTestObs(tx, fmt.Sprintf("obs-%d", i%10), fmt.Sprintf(`["p%d"]`, i)) {
t.Fatalf("observation %d rejected", i)
}
}
if tx.obsKeys == nil || tx.observerSet == nil {
t.Fatal("a busy transmission should index its observations")
}
if tx.UniqueObserverCount != 10 || tx.ObservationCount != n {
t.Fatalf("unique=%d count=%d, want 10 and %d", tx.UniqueObserverCount, tx.ObservationCount, n)
}
for i := 0; i < n; i++ {
if addTestObs(tx, fmt.Sprintf("obs-%d", i%10), fmt.Sprintf(`["p%d"]`, i)) {
t.Fatalf("duplicate %d accepted after the threshold", i)
}
}
if !addTestObs(tx, "obs-new", `["p0"]`) || tx.UniqueObserverCount != 11 {
t.Fatalf("new observer: unique=%d, want 11", tx.UniqueObserverCount)
}
}
// TestObsDedupEmptyObserverNotCounted: an observation without an observer
// ID is kept but does not count as a distinct observer.
func TestObsDedupEmptyObserverNotCounted(t *testing.T) {
tx := &StoreTx{ID: 1}
addTestObs(tx, "", `["a"]`)
addTestObs(tx, "", `["b"]`)
if tx.UniqueObserverCount != 0 || tx.ObservationCount != 2 {
t.Fatalf("unique=%d count=%d, want 0 and 2", tx.UniqueObserverCount, tx.ObservationCount)
}
}
func TestObsDedupUnionsRelayEvidenceAcrossThreshold(t *testing.T) {
for _, count := range []int{4, obsIndexThreshold, obsIndexThreshold * 2} {
t.Run(fmt.Sprintf("observations=%d", count), func(t *testing.T) {
tx := &StoreTx{}
for i := 0; i < count; i++ {
addTestObs(tx, fmt.Sprintf("obs-%d", i), `["4F2A"]`)
}
id := fmt.Sprintf("obs-%d", count-1)
for _, n := range []uint8{4, 0, 4} {
obs := tx.mergeObservationEvidence(id, `["4F2A"]`, n)
if obs != tx.Observations[count-1] || obs.relayHopHexLen != 4 {
t.Fatal("dedup must upgrade the existing observation and never erase evidence")
}
}
if tx.mergeObservationEvidence("absent", `["4F2A"]`, 4) != nil || len(tx.Observations) != count {
t.Fatal("evidence union must not create observations")
}
})
}
}
// BenchmarkObsDedup measures a miss (the common case on ingest) at sizes
// either side of the threshold.
func BenchmarkObsDedup(b *testing.B) {
for _, obsCount := range []int{4, 16, 40, 63, 64, 500} {
b.Run(fmt.Sprintf("obs=%d", obsCount), func(b *testing.B) {
tx := &StoreTx{ID: 1}
for i := 0; i < obsCount; i++ {
addTestObs(tx, fmt.Sprintf("%064d", i), fmt.Sprintf(`["hop-%d"]`, i))
}
newID := fmt.Sprintf("%064d", obsCount+1)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = tx.hasObservation(newID, `["new-path"]`)
}
})
}
}