Merge pull request #510 from sylvesterkaczmarek/fix/chain-majority-time-invalid-timestamps

fix(infonet): ignore invalid timestamps in majority time
This commit is contained in:
Shadowbroker
2026-08-15 21:43:05 -06:00
committed by GitHub
2 changed files with 72 additions and 18 deletions
@@ -0,0 +1,38 @@
"""Regression coverage for malformed chain timestamps."""
from typing import Any
from services.infonet.time_validity import chain_majority_time, is_event_too_future
def _event(node_id: str, timestamp: Any) -> dict[str, Any]:
return {"node_id": node_id, "timestamp": timestamp}
def test_chain_majority_time_ignores_malformed_and_nonfinite_timestamps() -> None:
chain = [
_event("valid-a", 10.0),
_event("bad-text", "not-a-timestamp"),
_event("bad-nan", float("nan")),
_event("bad-inf", float("inf")),
_event("valid-b", 20.0),
]
assert chain_majority_time(chain) == 15.0
def test_chain_majority_time_accepts_finite_numeric_strings() -> None:
chain = [_event("a", "10.5"), _event("b", "20.5")]
assert chain_majority_time(chain) == 15.5
def test_future_check_fails_closed_for_invalid_timestamps() -> None:
assert is_event_too_future(_event("bad-text", "not-a-timestamp"), chain_time=100.0)
assert is_event_too_future(_event("bad-nan", float("nan")), chain_time=100.0)
assert is_event_too_future(_event("bad-inf", float("inf")), chain_time=100.0)
def test_future_check_preserves_valid_timestamp_behavior() -> None:
assert not is_event_too_future(_event("near", 110.0), chain_time=100.0)
assert is_event_too_future(_event("far", 10_000.0), chain_time=100.0)
+34 -18
View File
@@ -33,6 +33,7 @@ responsibility.
from __future__ import annotations
import math
import statistics
from typing import Any, Iterable
@@ -47,6 +48,17 @@ from services.infonet.config import CONFIG
_DEFAULT_MEDIAN_N = 11
def _finite_timestamp(value: Any) -> float | None:
"""Return a finite numeric timestamp, or ``None`` when unusable."""
try:
timestamp = float(value)
except (TypeError, ValueError, OverflowError):
return None
if not math.isfinite(timestamp):
return None
return timestamp
def chain_majority_time(
chain: Iterable[dict[str, Any]],
*,
@@ -64,22 +76,27 @@ def chain_majority_time(
"""
if n <= 0:
raise ValueError("n must be positive")
events = [e for e in chain if isinstance(e, dict)]
events.sort(key=lambda e: float(e.get("timestamp") or 0.0), reverse=True)
events: list[tuple[float, dict[str, Any]]] = []
for event in chain:
if not isinstance(event, dict):
continue
timestamp = _finite_timestamp(event.get("timestamp"))
if timestamp is None:
continue
events.append((timestamp, event))
events.sort(key=lambda item: item[0], reverse=True)
seen_nodes: set[str] = set()
timestamps: list[float] = []
for ev in events:
node = ev.get("node_id")
for timestamp, event in events:
node = event.get("node_id")
if not isinstance(node, str) or not node:
continue
if node in seen_nodes:
continue
seen_nodes.add(node)
ts = ev.get("timestamp")
try:
timestamps.append(float(ts))
except (TypeError, ValueError):
continue
timestamps.append(timestamp)
if len(timestamps) >= n:
break
if not timestamps:
@@ -93,24 +110,23 @@ def is_event_too_future(
*,
chain_time: float | None = None,
) -> bool:
"""Is ``event.timestamp`` more than ``max_future_event_drift_sec``
ahead of ``chain_majority_time``?
"""Is ``event.timestamp`` invalid or beyond allowed future drift?
Pass ``chain_time`` when the caller has already computed it (e.g.
bulk validation of a batch — avoids recomputing the median per
event). Otherwise pass ``chain``.
Invalid/non-finite event timestamps fail closed: they return
``True`` so callers reject or re-queue them rather than allowing
malformed events through the drift gate.
"""
if chain_time is None:
if chain is None:
raise ValueError("Pass chain or chain_time")
chain_time = chain_majority_time(chain)
try:
ts = float(event.get("timestamp"))
except (TypeError, ValueError):
# Non-numeric timestamp is its own validation failure — let the
# schema-level check catch that. Drift check itself returns
# False here (we cannot meaningfully compare).
return False
ts = _finite_timestamp(event.get("timestamp"))
if ts is None:
return True
drift = float(CONFIG["max_future_event_drift_sec"])
return ts > chain_time + drift