From efa9da58b55876923fbca41f8037a5c592dae9e8 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:11:04 +0100 Subject: [PATCH 1/4] fix(infonet): ignore invalid timestamps in majority time --- backend/services/infonet/time_validity.py | 45 +++++++++++++++-------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/backend/services/infonet/time_validity.py b/backend/services/infonet/time_validity.py index 725c657..76f5efb 100644 --- a/backend/services/infonet/time_validity.py +++ b/backend/services/infonet/time_validity.py @@ -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: @@ -104,12 +121,10 @@ def is_event_too_future( 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). + ts = _finite_timestamp(event.get("timestamp")) + if ts is None: + # Non-numeric or non-finite timestamps are their own validation + # failure. Drift checking cannot meaningfully compare them. return False drift = float(CONFIG["max_future_event_drift_sec"]) return ts > chain_time + drift From 7f407d2e120cfb3926e744015805821a3d3470ce Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:11:14 +0100 Subject: [PATCH 2/4] test(infonet): cover invalid majority-time timestamps --- .../test_time_validity_invalid_timestamps.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/services/infonet/tests/test_time_validity_invalid_timestamps.py diff --git a/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py b/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py new file mode 100644 index 0000000..253cf07 --- /dev/null +++ b/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py @@ -0,0 +1,30 @@ +"""Regression coverage for malformed chain timestamps.""" + +from services.infonet.time_validity import chain_majority_time, is_event_too_future + + +def _event(node_id: str, timestamp): + return {"node_id": node_id, "timestamp": timestamp} + + +def test_chain_majority_time_ignores_malformed_and_nonfinite_timestamps(): + 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(): + chain = [_event("a", "10.5"), _event("b", "20.5")] + + assert chain_majority_time(chain) == 15.5 + + +def test_future_check_does_not_treat_nonfinite_timestamp_as_valid(): + assert not is_event_too_future(_event("a", float("nan")), chain_time=100.0) + assert not is_event_too_future(_event("a", float("inf")), chain_time=100.0) From 926f0f0c639c719bb603eea3e2b82f5e4ab1de30 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:31:30 +0100 Subject: [PATCH 3/4] Fail closed on invalid event timestamps --- backend/services/infonet/time_validity.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/services/infonet/time_validity.py b/backend/services/infonet/time_validity.py index 76f5efb..f556056 100644 --- a/backend/services/infonet/time_validity.py +++ b/backend/services/infonet/time_validity.py @@ -110,12 +110,15 @@ 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: @@ -123,9 +126,7 @@ def is_event_too_future( chain_time = chain_majority_time(chain) ts = _finite_timestamp(event.get("timestamp")) if ts is None: - # Non-numeric or non-finite timestamps are their own validation - # failure. Drift checking cannot meaningfully compare them. - return False + return True drift = float(CONFIG["max_future_event_drift_sec"]) return ts > chain_time + drift From 40f6e0c6f72c35001e1b11fa09c7396d88a87d1e Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:31:45 +0100 Subject: [PATCH 4/4] Cover fail-closed timestamp validation --- .../test_time_validity_invalid_timestamps.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py b/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py index 253cf07..065e65d 100644 --- a/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py +++ b/backend/services/infonet/tests/test_time_validity_invalid_timestamps.py @@ -1,13 +1,15 @@ """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): +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(): +def test_chain_majority_time_ignores_malformed_and_nonfinite_timestamps() -> None: chain = [ _event("valid-a", 10.0), _event("bad-text", "not-a-timestamp"), @@ -19,12 +21,18 @@ def test_chain_majority_time_ignores_malformed_and_nonfinite_timestamps(): assert chain_majority_time(chain) == 15.0 -def test_chain_majority_time_accepts_finite_numeric_strings(): +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_does_not_treat_nonfinite_timestamp_as_valid(): - assert not is_event_too_future(_event("a", float("nan")), chain_time=100.0) - assert not is_event_too_future(_event("a", float("inf")), chain_time=100.0) +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)