diff --git a/backend/services/infonet/gates/locking.py b/backend/services/infonet/gates/locking.py index d1593ae..0ed967e 100644 --- a/backend/services/infonet/gates/locking.py +++ b/backend/services/infonet/gates/locking.py @@ -26,6 +26,7 @@ production should run these *before* emitting): from __future__ import annotations +import math from dataclasses import dataclass from typing import Any, Iterable @@ -81,12 +82,18 @@ def _collect_lock_contributions( p = _payload(ev) try: paid = float(p.get("lock_cost") or 0.0) - except (TypeError, ValueError): - paid = 0.0 - if paid < float(_lock_cost_per_member()): + except (TypeError, ValueError, OverflowError): + continue + if not math.isfinite(paid) or paid < float(_lock_cost_per_member()): + continue + try: + timestamp = float(ev.get("timestamp") or 0.0) + except (TypeError, ValueError, OverflowError): + continue + if not math.isfinite(timestamp): continue seen.add(node) - out.append((node, float(ev.get("timestamp") or 0.0))) + out.append((node, timestamp)) return out @@ -133,7 +140,10 @@ def validate_lock_request( needs to change. """ chain_list = list(chain) - cost = int(_lock_cost_per_member() if lock_cost is None else lock_cost) + try: + cost = int(_lock_cost_per_member() if lock_cost is None else lock_cost) + except (TypeError, ValueError, OverflowError): + return LockValidation(False, "invalid_lock_cost", 0) if cost < _lock_cost_per_member(): return LockValidation(False, "lock_cost_below_min", cost) if node_id not in compute_member_set(gate_id, chain_list): diff --git a/backend/services/infonet/tests/test_gate_lock_nonfinite_values.py b/backend/services/infonet/tests/test_gate_lock_nonfinite_values.py new file mode 100644 index 0000000..78dbfbc --- /dev/null +++ b/backend/services/infonet/tests/test_gate_lock_nonfinite_values.py @@ -0,0 +1,66 @@ +"""Regression coverage for non-finite gate lock contributions.""" + +from services.infonet.config import CONFIG +from services.infonet.gates import is_locked, validate_lock_request +from services.infonet.tests._gate_factory import ( + make_gate_create, + make_gate_enter, + make_gate_lock, +) + + +def _gate_with_members() -> tuple[list[dict], list[str]]: + base = 1_000_000.0 + threshold = int(CONFIG["gate_lock_min_members"]) + members = [f"m{i}" for i in range(threshold)] + chain = [make_gate_create("g1", "creator", ts=base, seq=1)] + for i, member in enumerate(members): + chain.append(make_gate_enter("g1", member, ts=base + 100 + i, seq=2 + i)) + return chain, members + + +def _append_valid_locks(chain: list[dict], members: list[str]) -> None: + cost = int(CONFIG["gate_lock_cost_per_member"]) + for i, member in enumerate(members): + chain.append( + make_gate_lock( + "g1", + member, + ts=1_001_000.0 + i, + seq=200 + i, + lock_cost=cost, + ) + ) + + +def test_infinite_lock_cost_does_not_count_toward_threshold(): + chain, members = _gate_with_members() + _append_valid_locks(chain, members[:-1]) + bad = make_gate_lock("g1", members[-1], ts=1_002_000.0, seq=999) + bad["payload"]["lock_cost"] = float("inf") + chain.append(bad) + + assert not is_locked("g1", chain) + + +def test_nonfinite_lock_timestamp_does_not_count_toward_threshold(): + chain, members = _gate_with_members() + _append_valid_locks(chain, members[:-1]) + bad = make_gate_lock("g1", members[-1], ts=float("nan"), seq=999) + chain.append(bad) + + assert not is_locked("g1", chain) + + +def test_validate_lock_request_rejects_nonfinite_cost(): + chain, members = _gate_with_members() + + decision = validate_lock_request( + members[0], + "g1", + chain, + lock_cost=float("inf"), # type: ignore[arg-type] + ) + + assert not decision.accepted + assert decision.reason == "invalid_lock_cost"