mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 02:03:35 +00:00
fix: normalize configured_scope_at to canonical UTC before last-write-wins (#1865)
UpdateNodeConfiguredScope stored the report timestamp raw and compared it
lexicographically in the last-write-wins guard. Real firmware emits e.g.
"2026-07-26T09:43:48.000000+00:00" (microseconds + numeric offset), and other
observers may emit "Z" or a non-UTC offset like "+02:00". String-comparing
those raw diverges from chronological order across observers/formats, so a
stale report could overwrite a newer confirmed scope.
normalizeReportTS parses the timestamp (RFC3339Nano/RFC3339) and stores it as
canonical UTC RFC3339 ("...Z"), used for both storage and the comparison, so
every stored configured_scope_at is either canonical or empty — never a mix.
An unparseable/empty input yields "" and skips the ordering guard (writes),
preserving prior behavior.
New test TestUpdateNodeConfiguredScopeNormalizesAndOrdersByInstant proves a
"+02:00" report that is lexicographically greater but chronologically earlier
than the stored UTC value does not win, that a later "+02:00" report does, and
that the firmware's microsecond/"+00:00" form canonicalizes to "Z".
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+24
-1
@@ -1625,6 +1625,25 @@ func (s *Store) UpdateNodeDefaultScope(pubkey, scope string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// normalizeReportTS parses an observer report timestamp and returns it in
|
||||
// canonical UTC RFC3339 form ("2006-01-02T15:04:05Z"). It accepts both the
|
||||
// firmware's fractional/offset form (e.g. "2026-07-26T09:43:48.000000+00:00")
|
||||
// and plain "Z"/offset variants. An empty or unparseable input returns "" so
|
||||
// the caller writes an empty configured_scope_at and skips the ordering guard;
|
||||
// this keeps every stored timestamp either canonical or empty, never a mix of
|
||||
// offset/precision formats that would break lexicographic last-write-wins.
|
||||
func normalizeReportTS(raw string) string {
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
for _, layout := range []string{time.RFC3339Nano, time.RFC3339} {
|
||||
if t, err := time.Parse(layout, raw); err == nil {
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UpdateNodeConfiguredScope records the region scopes a node has CONFIGURED,
|
||||
// as concrete evidence from an observer /neighbors report (#1865). Unlike
|
||||
// UpdateNodeDefaultScope (inferred, overwritten on every observation), this is
|
||||
@@ -1641,11 +1660,15 @@ func (s *Store) UpdateNodeConfiguredScope(pubkey, scope, reportedAt string) erro
|
||||
if pubkey == "" {
|
||||
return nil
|
||||
}
|
||||
// Normalize to canonical UTC RFC3339 so the last-write-wins comparison is
|
||||
// chronological, not lexicographic (see normalizeReportTS). Stored values
|
||||
// are therefore always canonical or empty.
|
||||
reportedAt = normalizeReportTS(reportedAt)
|
||||
// Last-write-wins: skip if the stored confirmation is newer-or-equal.
|
||||
if reportedAt != "" {
|
||||
var curAt sql.NullString
|
||||
row := s.db.QueryRow(`SELECT configured_scope_at FROM nodes WHERE public_key = ?`, pubkey)
|
||||
if row.Scan(&curAt) == nil && curAt.Valid && curAt.String >= reportedAt {
|
||||
if row.Scan(&curAt) == nil && curAt.Valid && curAt.String != "" && curAt.String >= reportedAt {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +158,52 @@ func TestUpdateNodeConfiguredScopeLastWriteWins(t *testing.T) {
|
||||
t.Errorf("inactive_nodes.configured_scope = %q, want 'de'", inactive.String)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateNodeConfiguredScopeNormalizesAndOrdersByInstant proves the
|
||||
// last-write-wins guard orders by chronological instant, not by raw string.
|
||||
// A "+02:00" report that is lexicographically "greater" but chronologically
|
||||
// EARLIER than the stored UTC value must not win, and stored timestamps are
|
||||
// canonicalized to UTC "Z" form regardless of the incoming offset/precision.
|
||||
func TestUpdateNodeConfiguredScopeNormalizesAndOrdersByInstant(t *testing.T) {
|
||||
store := openNeighborsStore(t)
|
||||
pk := "ee00000000000000000000000000000000000000000000000000000000000001"
|
||||
seedNode(t, store, pk)
|
||||
|
||||
// Baseline: noon UTC.
|
||||
if err := store.UpdateNodeConfiguredScope(pk, "eu", "2026-07-25T12:00:00Z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, at := configuredScope(t, store, pk); at.String != "2026-07-25T12:00:00Z" {
|
||||
t.Fatalf("stored at = %q, want canonical 'Z' form", at.String)
|
||||
}
|
||||
|
||||
// "2026-07-25T13:30:00+02:00" == 11:30Z, chronologically EARLIER than 12:00Z,
|
||||
// but lexicographically GREATER ("13:30..." > "12:00...Z"). Must be skipped.
|
||||
if err := store.UpdateNodeConfiguredScope(pk, "stale", "2026-07-25T13:30:00+02:00"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sc, _ := configuredScope(t, store, pk); sc.String != "eu" {
|
||||
t.Errorf("configured_scope = %q, want 'eu' (earlier +02:00 report must not win)", sc.String)
|
||||
}
|
||||
|
||||
// "2026-07-25T15:00:00+02:00" == 13:00Z, chronologically LATER. Must update,
|
||||
// and be stored canonicalized to UTC.
|
||||
if err := store.UpdateNodeConfiguredScope(pk, "de", "2026-07-25T15:00:00+02:00"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sc, at := configuredScope(t, store, pk)
|
||||
if sc.String != "de" {
|
||||
t.Errorf("configured_scope = %q, want 'de' (later +02:00 report should win)", sc.String)
|
||||
}
|
||||
if at.String != "2026-07-25T13:00:00Z" {
|
||||
t.Errorf("stored at = %q, want canonical '2026-07-25T13:00:00Z'", at.String)
|
||||
}
|
||||
|
||||
// Firmware's real format (microseconds + "+00:00") canonicalizes to "Z".
|
||||
if err := store.UpdateNodeConfiguredScope(pk, "dk", "2026-07-26T09:43:48.000000+00:00"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, at := configuredScope(t, store, pk); at.String != "2026-07-26T09:43:48Z" {
|
||||
t.Errorf("stored at = %q, want canonical '2026-07-26T09:43:48Z'", at.String)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user