diff --git a/openpilot/starpilot/flm/live_tracking_response.py b/openpilot/starpilot/flm/live_tracking_response.py index 3a4df3fb2e..7bf546af73 100644 --- a/openpilot/starpilot/flm/live_tracking_response.py +++ b/openpilot/starpilot/flm/live_tracking_response.py @@ -138,11 +138,11 @@ def _source_key(context): _VALID_SOURCE_CONTEXTS = OrderedDict() -def validate_source_context(context): +def _validated_source_key(context): key = _source_key(context) if key in _VALID_SOURCE_CONTEXTS: _VALID_SOURCE_CONTEXTS.move_to_end(key) - return context + return key context.binding.validate() if ( context.version != SOURCE_VERSION @@ -165,6 +165,11 @@ def validate_source_context(context): _VALID_SOURCE_CONTEXTS[key] = None if len(_VALID_SOURCE_CONTEXTS) > 8: _VALID_SOURCE_CONTEXTS.popitem(last=False) + return key + + +def validate_source_context(context): + _validated_source_key(context) return context @@ -424,11 +429,23 @@ class ResponseWitness: def _validate_raw_witness(witness, binding, now_ns, *, content=True): if type(witness) is not ResponseWitness: raise ValueError('fresh source physical residual witness required') - cert = validate_certificate(witness.certificate) if content else witness.certificate + owned = _owned(witness, ResponseWitness, _WITNESS_FIELDS) + cert = owned['certificate'] + source_key = _validated_source_key(cert) if content and type(cert) is TrackingSourceContext else None + if content and source_key is None: + cert = validate_certificate(cert) source_only = type(cert) is TrackingSourceContext + # Share immutable values only within this call. Every new call still checks + # the actual dictionaries/accessors, expected binding and original clocks. + cert_binding = cert.binding + cert_values = source_key[1] if source_key is not None else _binding_values(cert_binding) + expected = cert_values if binding is cert_binding else _binding_values(binding) + actual_binding = owned['binding'] + actual = (cert_values if actual_binding is cert_binding else + expected if actual_binding is binding else _binding_values(actual_binding)) if ( - _binding_values(witness.binding) != _binding_values(binding) - or _binding_values(cert.binding) != _binding_values(binding) + actual != expected + or cert_values != expected or any( type(v) is not int for v in ( diff --git a/openpilot/starpilot/flm/tests/test_witness_binding_values.py b/openpilot/starpilot/flm/tests/test_witness_binding_values.py new file mode 100644 index 0000000000..72ed2ef529 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_witness_binding_values.py @@ -0,0 +1,35 @@ +"""Per-call binding validation deduplicates identities, never clocks or grants.""" +from dataclasses import replace +import pytest +from openpilot.starpilot.flm import live_tracking_response as response +from openpilot.starpilot.flm.tests.test_tracking_source_context import context + + +@pytest.mark.parametrize('same_witness,same_expected', [(True, True), (False, True), (True, False), (False, False)]) +def test_each_distinct_binding_validated_once_per_witness_call(monkeypatch, same_witness, same_expected): + certificate = context() + now = 10_000_000_000 + actual = certificate.binding if same_witness else replace(certificate.binding) + expected = certificate.binding if same_expected else replace(certificate.binding) + raw = response.ResponseWitness(certificate, actual, now-certificate.lag_ns, + now-certificate.lag_ns, now, now-350_000_000, 8, None, now+100_000_000) + calls = [] + original = response._binding_values + def measured(value): + calls.append(value) + return original(value) + monkeypatch.setattr(response, '_binding_values', measured) + assert response.validate_response_witness(raw, expected, now) is raw + assert len(calls) == len({id(actual), id(expected), id(certificate.binding)}) + + +def test_binding_deduplication_does_not_reuse_a_previous_call(): + certificate = context() + now = 10_000_000_000 + raw = response.ResponseWitness(certificate, certificate.binding, now-certificate.lag_ns, + now-certificate.lag_ns, now, now-350_000_000, 8, None, now+100_000_000) + assert response.validate_response_witness(raw, certificate.binding, now) is raw + with pytest.raises(ValueError): + response.validate_response_witness(raw, certificate.binding, now+100_000_001) + with pytest.raises(ValueError): + response.validate_response_witness(raw, replace(certificate.binding, source_epoch='0'*64), now) diff --git a/prebuilt.json b/prebuilt.json index 4d0fa511d0..23437ff0dc 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "7f8eb67a69cd1ecbdcbabd4305fd8e2cb8c86c0e1185710d118e22210091159a", "schema_version": 1} +{"protected_inventory_sha256": "df20c562268a9a77c5f28cebfefaf91594010416c0a50ef59bdcf8f3bc759bb5", "schema_version": 1}