mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 17:07:53 +00:00
Follow-up to #1823: TestRepeaterUnscopedRelayCount and its _Bulk twin were ~30 verbatim lines apart (DB, node insert, store seeding, assertions), differing only in the lookup under test - seeding changes had to land twice. Both now use a shared seedUnscopedRelayFixture + assertUnscopedCounts and contain only their respective lookup call. No behaviour change; the relay-liveness suite passes. Co-authored-by: Waydroid Builder <build@waydroid.local>
322 lines
12 KiB
Go
322 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestRepeaterRelayActivity_Active verifies that a repeater whose pubkey
|
|
// appears as a relay hop in a recent (non-advert) packet is reported with
|
|
// a non-zero lastRelayed timestamp and relayActive=true.
|
|
func TestRepeaterRelayActivity_Active(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "aabbccdd11223344"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepActive", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
// A non-advert packet (payload_type=1, TXT_MSG) with the repeater pubkey
|
|
// indexed as a path hop. Index by lowercase pubkey directly to mirror
|
|
// the resolved-path entries that decode-window writes.
|
|
pt := 1
|
|
relayed := &StoreTx{
|
|
RawHex: "0100",
|
|
PayloadType: &pt,
|
|
PathJSON: `["aa"]`,
|
|
FirstSeen: recentTS(2),
|
|
}
|
|
store.mu.Lock()
|
|
relayed.ID = len(store.packets) + 1
|
|
relayed.Hash = "test-relay-1"
|
|
store.packets = append(store.packets, relayed)
|
|
store.byHash[relayed.Hash] = relayed
|
|
store.byTxID[relayed.ID] = relayed
|
|
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], relayed)
|
|
store.mu.Unlock()
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.LastRelayed == "" {
|
|
t.Fatalf("expected non-empty LastRelayed for active relayer, got empty (RelayActive=%v)", info.RelayActive)
|
|
}
|
|
if !info.RelayActive {
|
|
t.Errorf("expected RelayActive=true within 24h window, got false (LastRelayed=%s)", info.LastRelayed)
|
|
}
|
|
if info.RelayCount1h != 0 {
|
|
t.Errorf("expected RelayCount1h=0 (relay was 2h ago, outside 1h window), got %d", info.RelayCount1h)
|
|
}
|
|
if info.RelayCount24h != 1 {
|
|
t.Errorf("expected RelayCount24h=1 (relay was 2h ago, inside 24h window), got %d", info.RelayCount24h)
|
|
}
|
|
}
|
|
|
|
// seedUnscopedRelayFixture builds a store in which pubkey appears as a relay
|
|
// hop on one FLOOD (unscoped) and one DIRECT tx - the shared fixture for the
|
|
// per-node and bulk UnscopedRelayCount24h tests, so the seeding pattern cannot
|
|
// drift between the two. hashPrefix keeps the packet hashes distinguishable.
|
|
func seedUnscopedRelayFixture(t *testing.T, hashPrefix string) (*PacketStore, string, func()) {
|
|
t.Helper()
|
|
db := setupCapabilityTestDB(t)
|
|
pubkey := "aabbccdd11223344"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepUnscoped", "repeater", recentTS(1))
|
|
store := NewPacketStore(db, nil)
|
|
pt := 1 // non-advert (TXT_MSG)
|
|
flood := routeTypeFlood
|
|
direct := 2 // ROUTE_TYPE_DIRECT - scoped/directed, must NOT count as unscoped
|
|
mk := func(hash string, rt int) *StoreTx {
|
|
return &StoreTx{RawHex: "0100", PayloadType: &pt, RouteType: &rt, PathJSON: `["aa"]`, FirstSeen: recentTS(2), Hash: hash}
|
|
}
|
|
store.mu.Lock()
|
|
for _, tx := range []*StoreTx{mk(hashPrefix+"unscoped-1", flood), mk(hashPrefix+"direct-1", direct)} {
|
|
tx.ID = len(store.packets) + 1
|
|
store.packets = append(store.packets, tx)
|
|
store.byHash[tx.Hash] = tx
|
|
store.byTxID[tx.ID] = tx
|
|
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], tx)
|
|
}
|
|
store.mu.Unlock()
|
|
return store, pubkey, func() { db.conn.Close() }
|
|
}
|
|
|
|
// assertUnscopedCounts pins the contract both lookups share: FLOOD hops count
|
|
// as unscoped, DIRECT hops only as plain relays.
|
|
func assertUnscopedCounts(t *testing.T, info RepeaterRelayInfo) {
|
|
t.Helper()
|
|
if info.RelayCount24h != 2 {
|
|
t.Errorf("expected RelayCount24h=2 (both hops), got %d", info.RelayCount24h)
|
|
}
|
|
if info.UnscopedRelayCount24h != 1 {
|
|
t.Errorf("expected UnscopedRelayCount24h=1 (only the FLOOD hop), got %d", info.UnscopedRelayCount24h)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterUnscopedRelayCount verifies that UnscopedRelayCount24h counts only
|
|
// route_type==FLOOD (unscoped) relay hops, as a subset of RelayCount24h.
|
|
func TestRepeaterUnscopedRelayCount(t *testing.T) {
|
|
store, pubkey, done := seedUnscopedRelayFixture(t, "")
|
|
defer done()
|
|
assertUnscopedCounts(t, store.GetRepeaterRelayInfo(pubkey, 24))
|
|
}
|
|
|
|
// TestRepeaterRelayActivity_Idle verifies that a repeater whose pubkey
|
|
// has not appeared as a relay hop reports an empty LastRelayed and
|
|
// relayActive=false.
|
|
func TestRepeaterRelayActivity_Idle(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "ccddeeff55667788"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepIdle", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.LastRelayed != "" {
|
|
t.Errorf("expected empty LastRelayed for idle repeater, got %q", info.LastRelayed)
|
|
}
|
|
if info.RelayActive {
|
|
t.Errorf("expected RelayActive=false for idle repeater, got true")
|
|
}
|
|
if info.RelayCount1h != 0 || info.RelayCount24h != 0 {
|
|
t.Errorf("expected zero relay counts for idle repeater, got 1h=%d 24h=%d", info.RelayCount1h, info.RelayCount24h)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterRelayActivity_Stale verifies that a repeater whose only
|
|
// relay-hop appearances are older than the configured window reports
|
|
// a non-empty LastRelayed but relayActive=false.
|
|
func TestRepeaterRelayActivity_Stale(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "1122334455667788"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepStale", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
pt := 1
|
|
staleTS := time.Now().UTC().Add(-48 * time.Hour).Format("2006-01-02T15:04:05.000Z")
|
|
old := &StoreTx{
|
|
RawHex: "0100",
|
|
PayloadType: &pt,
|
|
PathJSON: `["11"]`,
|
|
FirstSeen: staleTS,
|
|
}
|
|
store.mu.Lock()
|
|
old.ID = len(store.packets) + 1
|
|
old.Hash = "test-relay-stale"
|
|
store.packets = append(store.packets, old)
|
|
store.byHash[old.Hash] = old
|
|
store.byTxID[old.ID] = old
|
|
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], old)
|
|
store.mu.Unlock()
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.LastRelayed != staleTS {
|
|
t.Errorf("expected LastRelayed=%q (stale ts), got %q", staleTS, info.LastRelayed)
|
|
}
|
|
if info.RelayActive {
|
|
t.Errorf("expected RelayActive=false for relay older than window, got true")
|
|
}
|
|
if info.RelayCount1h != 0 || info.RelayCount24h != 0 {
|
|
t.Errorf("expected zero relay counts for stale (>24h) repeater, got 1h=%d 24h=%d", info.RelayCount1h, info.RelayCount24h)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterRelayActivity_IgnoresAdverts verifies that adverts originated
|
|
// by the repeater itself (payload_type=4) are NOT counted as relay activity —
|
|
// adverts demonstrate liveness, not relaying.
|
|
func TestRepeaterRelayActivity_IgnoresAdverts(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "deadbeef00000001"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepAdvertOnly", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
// Self-advert with the repeater as its own first hop. Should NOT count.
|
|
pt := 4
|
|
adv := &StoreTx{
|
|
RawHex: "0140de",
|
|
PayloadType: &pt,
|
|
PathJSON: `["de"]`,
|
|
FirstSeen: recentTS(2),
|
|
}
|
|
store.mu.Lock()
|
|
adv.ID = len(store.packets) + 1
|
|
adv.Hash = "test-advert-1"
|
|
store.packets = append(store.packets, adv)
|
|
store.byHash[adv.Hash] = adv
|
|
store.byTxID[adv.ID] = adv
|
|
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], adv)
|
|
store.mu.Unlock()
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.LastRelayed != "" {
|
|
t.Errorf("expected empty LastRelayed (adverts ignored), got %q", info.LastRelayed)
|
|
}
|
|
if info.RelayActive {
|
|
t.Errorf("expected RelayActive=false (adverts ignored), got true")
|
|
}
|
|
if info.RelayCount1h != 0 || info.RelayCount24h != 0 {
|
|
t.Errorf("expected zero relay counts (adverts ignored), got 1h=%d 24h=%d", info.RelayCount1h, info.RelayCount24h)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterRelayActivity_PrefixHop verifies that GetRepeaterRelayInfo
|
|
// counts a non-advert packet whose path contains only the 1-byte raw hop
|
|
// prefix matching the target node (not the full resolved pubkey).
|
|
//
|
|
// Reality on prod/staging: many ingested packets only carry raw 1-byte
|
|
// path hops (e.g. ["a3"] from the wire) — resolution to a full pubkey
|
|
// happens later via neighbor affinity for the "Paths seen through node"
|
|
// view. The byPathHop index is populated under BOTH keys (raw hop AND
|
|
// resolved pubkey), but GetRepeaterRelayInfo only looks up the full
|
|
// pubkey, missing all raw-hop-only entries. This is the cause of the
|
|
// "never observed as relay hop" claim on nodes that clearly have paths
|
|
// shown through them. See https://analyzer-stg.00id.net/#/nodes/<pk>.
|
|
func TestRepeaterRelayActivity_PrefixHop(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "a36a21290d9c25a158130fe7c489541210d5f09f25fab997db5e942fb7680510"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepPrefix", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
// Non-advert packet with a single raw 1-byte hop matching the target
|
|
// pubkey's first byte ("a3"). Index it the way addTxToPathHopIndex
|
|
// does — under the raw hop key only, not the full pubkey.
|
|
pt := 1
|
|
tx := &StoreTx{
|
|
RawHex: "0100",
|
|
PayloadType: &pt,
|
|
PathJSON: `["a3"]`,
|
|
FirstSeen: recentTS(2),
|
|
}
|
|
store.mu.Lock()
|
|
tx.ID = len(store.packets) + 1
|
|
tx.Hash = "test-relay-prefix-1"
|
|
store.packets = append(store.packets, tx)
|
|
store.byHash[tx.Hash] = tx
|
|
store.byTxID[tx.ID] = tx
|
|
addTxToPathHopIndex(store.byPathHop, tx)
|
|
store.mu.Unlock()
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.RelayCount24h < 1 {
|
|
t.Fatalf("expected RelayCount24h>=1 for node with prefix-matched hop in path, got %d (LastRelayed=%q)",
|
|
info.RelayCount24h, info.LastRelayed)
|
|
}
|
|
if info.LastRelayed == "" {
|
|
t.Errorf("expected non-empty LastRelayed when prefix hop matched, got empty")
|
|
}
|
|
if !info.RelayActive {
|
|
t.Errorf("expected RelayActive=true within 24h window, got false (LastRelayed=%s)", info.LastRelayed)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterRelayActivity_DedupAcrossPrefixAndFullKey verifies that when
|
|
// the SAME packet is indexed in byPathHop under BOTH the full pubkey AND
|
|
// the raw 1-byte prefix, GetRepeaterRelayInfo counts it exactly once. This
|
|
// gates the `seen[tx.ID]` dedup map: without it, hop counts would double
|
|
// for any tx that resolved-path indexing recorded under both keys.
|
|
func TestRepeaterRelayActivity_DedupAcrossPrefixAndFullKey(t *testing.T) {
|
|
db := setupCapabilityTestDB(t)
|
|
defer db.conn.Close()
|
|
|
|
pubkey := "a36a21290d9c25a158130fe7c489541210d5f09f25fab997db5e942fb7680510"
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
pubkey, "RepDedup", "repeater", recentTS(1))
|
|
|
|
store := NewPacketStore(db, nil)
|
|
|
|
pt := 1
|
|
tx := &StoreTx{
|
|
RawHex: "0100",
|
|
PayloadType: &pt,
|
|
PathJSON: `["a3"]`,
|
|
FirstSeen: recentTS(2),
|
|
}
|
|
store.mu.Lock()
|
|
tx.ID = len(store.packets) + 1
|
|
tx.Hash = "test-relay-dedup-1"
|
|
store.packets = append(store.packets, tx)
|
|
store.byHash[tx.Hash] = tx
|
|
store.byTxID[tx.ID] = tx
|
|
// Index under BOTH the full pubkey AND the raw 1-byte prefix — this
|
|
// is the exact double-index case that occurs when wire ingest records
|
|
// the raw hop and a later resolution pass also records the full key.
|
|
store.byPathHop[pubkey] = append(store.byPathHop[pubkey], tx)
|
|
store.byPathHop[pubkey[:2]] = append(store.byPathHop[pubkey[:2]], tx)
|
|
store.mu.Unlock()
|
|
|
|
info := store.GetRepeaterRelayInfo(pubkey, 24)
|
|
if info.RelayCount24h != 1 {
|
|
t.Fatalf("expected RelayCount24h=1 (dedup across full+prefix indexing), got %d", info.RelayCount24h)
|
|
}
|
|
if info.RelayCount1h != 0 {
|
|
t.Errorf("expected RelayCount1h=0 (relay was 2h ago, outside 1h window), got %d", info.RelayCount1h)
|
|
}
|
|
if !info.RelayActive {
|
|
t.Errorf("expected RelayActive=true, got false (LastRelayed=%s)", info.LastRelayed)
|
|
}
|
|
}
|
|
|
|
// TestRepeaterUnscopedRelayCount_Bulk verifies the bulk /api/nodes path
|
|
// (computeRepeaterRelayInfoMap) counts unscoped floods identically to the
|
|
// per-node path: only route_type==FLOOD hops, as a subset of RelayCount24h.
|
|
func TestRepeaterUnscopedRelayCount_Bulk(t *testing.T) {
|
|
store, pubkey, done := seedUnscopedRelayFixture(t, "bulk-")
|
|
defer done()
|
|
assertUnscopedCounts(t, store.computeRepeaterRelayInfoMap(24)[pubkey])
|
|
}
|