Reuse exact Ioniq native structural admission per observation

Retain all live freshness, session, permission and original lease checks.
Reuse only successful structural validation for exact payload, timestamp
and complete CP inputs within each owner. Unsupported profiles fall back
to full validation. Add 50 invalidation and fallback regressions.

Validated affected AOL, SelfdriveD and Controls suites, unchanged Card
fixture failures, full lint/type checks and deterministic owner outputs.
Vehicle-level dropout resolution remains unverified.
This commit is contained in:
Dom
2026-10-11 15:48:58 -06:00
parent 805051be92
commit 46dd0df949
4 changed files with 353 additions and 4 deletions
+38 -4
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
from dataclasses import dataclass
from opendbc.car.structs import car
from openpilot.starpilot.aol.vehicle import native_matches_cp, native_profile_supported
from openpilot.starpilot.aol.vehicle import native_admission_key, native_matches_cp, native_profile_supported
from openpilot.starpilot.aol.wire import INTENT_SERVICE, SAFETY_SERVICE, IntentState, decode_intent, decode_safety
INTENT_MAX_AGE_NS = 30_000_000
@@ -105,23 +105,57 @@ def native_inventory_matches_cp(state, CP) -> bool:
return False
def _clear_native_admission(sm):
try:
sm._aol_native_admission = None
except Exception:
# Custom/slotted owners may not support this optional optimization.
pass
def _native_admission_matches(sm, CP, state, raw):
key = None
try:
cp_key = native_admission_key(CP) if state.pandaInventory else None
if cp_key is not None and isinstance(raw, bytes):
key = (raw, int(sm.logMonoTime[SAFETY_SERVICE]), cp_key)
if getattr(sm, '_aol_native_admission', None) == key:
return True
except Exception:
# Cache plumbing may never replace or prevent the original validation.
key = None
matches = (native_matches_cp(CP, int(state.safetyModel), int(state.safetyParam)) and
native_inventory_matches_cp(state, CP))
try:
# Re-read mutable CP on a miss before binding the successful result.
sm._aol_native_admission = key if matches and key is not None and native_admission_key(CP) == key[2] else None
except Exception:
_clear_native_admission(sm)
return matches
def current_native(sm, CP, *, now_ns: int, axis_session_id: str | None = None):
if not (sm.valid[SAFETY_SERVICE] and sm.alive[SAFETY_SERVICE] and sm.seen[SAFETY_SERVICE]):
_clear_native_admission(sm)
return None
state = decode_safety(sm[SAFETY_SERVICE])
raw = sm[SAFETY_SERVICE]
raw = bytes(raw) if isinstance(raw, (bytes, bytearray, memoryview)) else raw
state = decode_safety(raw)
if state is None:
_clear_native_admission(sm)
return None
if (not native_matches_cp(CP, int(state.safetyModel), int(state.safetyParam)) or
not native_inventory_matches_cp(state, CP) or
if (not _native_admission_matches(sm, CP, state, raw) or
not state.compatible or int(state.protocolVersion) != 1 or not state.pandaSerial or not state.axisSessionId or
(axis_session_id is not None and str(state.axisSessionId) != axis_session_id) or
int(sm.logMonoTime[SAFETY_SERVICE]) > now_ns or
now_ns - int(sm.logMonoTime[SAFETY_SERVICE]) > SAFETY_MAX_AGE_NS or
int(state.observedMonoTime) > now_ns or now_ns > int(state.validUntilMonoTime)):
_clear_native_admission(sm)
return None
if state.pandaInventory and (int(sm.logMonoTime[SAFETY_SERVICE]) != state.observedMonoTime or
state.observedMonoTime <= 0 or
state.validUntilMonoTime - state.observedMonoTime > SAFETY_MAX_AGE_NS):
_clear_native_admission(sm)
return None
return state
@@ -0,0 +1,293 @@
"""Reuse structural admission only; every live native gate remains per call."""
from dataclasses import replace
from unittest import mock
import pytest
from openpilot.cereal import messaging
from openpilot.starpilot.aol import runtime, transport_pause
from openpilot.starpilot.aol.tests.test_panda_inventory import NOW, bound_state, configured_cp, sm_for
from openpilot.starpilot.aol.tests.test_runtime import car_state
from openpilot.starpilot.aol.wire import encode_safety
def receive(sm, state, *, stamp=None, valid=True, raw=None):
stamp = state.observedMonoTime if stamp is None else stamp
message = messaging.new_message('aolSafetyWire', 0, valid=valid, logMonoTime=stamp)
message.aolSafetyWire = encode_safety(state) if raw is None else raw
sm.update_msgs(max(NOW, stamp) / 1e9, [message.as_reader()])
def current(sm, cp, now=NOW, session=None):
return runtime.current_native(sm, cp, now_ns=now, axis_session_id=session)
def test_twenty_observations_per_two_hundred_live_checks_and_owner_isolation():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
other = sm_for(cp, state)
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile, \
mock.patch.object(runtime, 'native_inventory_matches_cp', wraps=runtime.native_inventory_matches_cp) as inventory:
for tick in range(200):
now = NOW + tick * 10_000_000
if tick % 10 == 0:
state = bound_state(cp, now=now)
receive(sm, state)
assert current(sm, cp, now, state.axisSessionId if tick % 2 else None) == state
assert profile.call_count == inventory.call_count == 20
assert current(other, cp) is not None
assert profile.call_count == inventory.call_count == 21
assert len(sm._aol_native_admission) == 3
@pytest.mark.parametrize('field,value', [
('brand', 'mock'), ('carFingerprint', 'mock'), ('flags', 0), ('radarUnavailable', True),
('notCar', True), ('passive', True), ('dashcamOnly', True),
('openpilotLongitudinalControl', False), ('pcmCruise', True), ('alternativeExperience', 32),
('safetyModel', 0), ('safetyParam', 0), ('extra_slot', True),
])
def test_every_cp_admission_input_invalidates_a_hot_entry(field, value):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert current(sm, cp) is not None
original = cp.as_reader().as_builder()
if field == 'extra_slot':
cp.safetyConfigs = [original.safetyConfigs[0], original.safetyConfigs[0]]
elif field in ('safetyModel', 'safetyParam'):
setattr(cp.safetyConfigs[0], field, value)
else:
setattr(cp, field, value)
assert current(sm, cp) is None
assert sm._aol_native_admission is None
if field == 'extra_slot':
cp.safetyConfigs = [original.safetyConfigs[0]]
elif field in ('safetyModel', 'safetyParam'):
setattr(cp.safetyConfigs[0], field, getattr(original.safetyConfigs[0], field))
else:
setattr(cp, field, getattr(original, field))
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) is not None
assert profile.call_count == 1
@pytest.mark.parametrize('field', ['valid', 'alive', 'seen'])
def test_live_envelope_health_invalidates_hot_entry(field):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert current(sm, cp) is not None
getattr(sm, field)['aolSafetyWire'] = False
assert current(sm, cp) is None
assert sm._aol_native_admission is None
getattr(sm, field)['aolSafetyWire'] = True
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) is not None
assert profile.call_count == 1
@pytest.mark.parametrize('change', ['session', 'future_clock', 'expired', 'future_source', 'changed_source', 'bad_byte'])
def test_live_identity_and_original_lease_reject_hot_entry(change):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert current(sm, cp) is not None
now, session = NOW, state.axisSessionId
if change == 'session':
session = 'foreign'
elif change == 'future_clock':
now -= 1
elif change == 'expired':
now += 200_000_001
elif change in ('future_source', 'changed_source'):
sm.logMonoTime['aolSafetyWire'] += 1 if change == 'future_source' else -1
else:
raw = bytearray(encode_safety(state))
raw[0] ^= 1
receive(sm, state, raw=bytes(raw))
assert current(sm, cp, now, session) is None
assert sm._aol_native_admission is None
@pytest.mark.parametrize('change', ['lateral', 'longitudinal', 'rx', 'heartbeat', 'fault', 'serial', 'inventory', 'compatible', 'protocol'])
def test_changed_wire_cannot_borrow_previous_native_permission_or_health(change):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert current(sm, cp) is not None
values = {'lateral': {'lateralAllowed': False}, 'longitudinal': {'longitudinalAllowed': False},
'serial': {'pandaSerial': 'foreign'}, 'compatible': {'compatible': False}, 'protocol': {'protocolVersion': 2}}
if change in values:
updated = replace(state, **values[change])
else:
slot = state.pandaInventory[0]
fields = {'rx': {'safetyRxChecksInvalid': True}, 'heartbeat': {'heartbeatLost': True},
'fault': {'faults': 1}, 'inventory': {'safetyParam': 0}}
updated = replace(state, pandaInventory=(replace(slot, **fields[change]),))
receive(sm, updated)
result = current(sm, cp)
if change in ('lateral', 'longitudinal'):
assert result == updated
assert not getattr(result, 'lateralAllowed' if change == 'lateral' else 'longitudinalAllowed')
else:
assert result is None
assert sm._aol_native_admission is None
def test_latest_panda_fault_is_still_checked_on_a_native_cache_hit():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=state.sourcePandaStatesMonoTime)
pandas.pandaStates[0] = sm['pandaStates'][0]
pandas.pandaStates[0].heartbeatLost = True
sm.update_msgs(NOW / 1e9, [pandas.as_reader()])
assert current(sm, cp) is not None
assert not transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
def test_same_builder_mutation_during_miss_cannot_store_an_old_key():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
original = runtime.native_inventory_matches_cp
def validate_then_mutate(native, params):
result = original(native, params)
params.radarUnavailable = True
return result
with mock.patch.object(runtime, 'native_inventory_matches_cp', side_effect=validate_then_mutate):
assert current(sm, cp) is not None # Same result as the uncached validation call.
assert sm._aol_native_admission is None
assert current(sm, cp) is None
@pytest.mark.parametrize('legacy', [False, True])
def test_unsupported_owner_or_legacy_wire_keeps_full_validation(legacy):
cp = configured_cp()
state = bound_state(cp)
if legacy:
state = replace(state, sourcePandaStatesMonoTime=0, pandaInventory=())
original = sm_for(cp, state)
class SlottedOwner:
__slots__ = ('valid', 'alive', 'seen', 'logMonoTime')
def __init__(self):
for name in self.__slots__:
setattr(self, name, getattr(original, name))
def __getitem__(self, name):
return original[name]
sm = original if legacy else SlottedOwner()
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) is not None
assert current(sm, cp) is not None
assert profile.call_count == 2
@pytest.mark.parametrize('profile_name', ['honda', 'ev6_two_pandas'])
def test_other_admitted_profiles_keep_full_validation(profile_name):
if profile_name == 'honda':
from opendbc.car import gen_empty_fingerprint
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
fingerprint = gen_empty_fingerprint()
fingerprint[1][0x1A3] = 8
cp = CarInterface.get_params(CAR.HONDA_ACCORD, fingerprint, [], False, False, False)
cp.alternativeExperience = 32
else:
from openpilot.starpilot.tests.test_ev6_startup import params
cp = params()
cp.openpilotLongitudinalControl, cp.pcmCruise, cp.alternativeExperience = True, False, 32
cp.safetyConfigs = [{'safetyModel': 'noOutput', 'safetyParam': 0},
{'safetyModel': 'hyundaiCanfd', 'safetyParam': 0x815}]
state = bound_state(cp)
sm = sm_for(cp, state)
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) == state
assert current(sm, cp) == state
assert profile.call_count == 2
assert sm._aol_native_admission is None
def test_rejected_observation_then_valid_recovery_cannot_reuse_a_failed_verdict():
cp = configured_cp()
good = bound_state(cp)
bad = replace(good, pandaSerial='other')
sm = sm_for(cp, bad)
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) is None
assert current(sm, cp) is None
assert profile.call_count == 2
assert sm._aol_native_admission is None
receive(sm, good)
assert current(sm, cp) == good
assert current(sm, cp) == good
assert profile.call_count == 3
def test_cache_key_failure_falls_back_to_complete_validation():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert current(sm, cp) == state
with mock.patch.object(runtime, 'native_admission_key', side_effect=RuntimeError('cache failure')), \
mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) == state
assert current(sm, cp) == state
assert profile.call_count == 2
assert sm._aol_native_admission is None
@pytest.mark.parametrize('field,value', [('canValid', False), ('canTimeout', True),
('steerFaultTemporary', True), ('steerFaultPermanent', True)])
def test_live_car_state_veto_remains_after_native_cache_hit(field, value):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
cs = car_state()
assert transport_pause.healthy_transport(sm, cp, cs, state.axisSessionId, NOW)
setattr(cs, field, value)
assert current(sm, cp) == state
assert not transport_pause.healthy_transport(sm, cp, cs, state.axisSessionId, NOW)
@pytest.mark.parametrize('field,value', [('controlsAllowed', False), ('safetyParam', 0),
('alternativeExperience', 32), ('safetyRxChecksInvalid', True)])
def test_latest_panda_permission_and_configuration_veto_a_native_cache_hit(field, value):
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
assert transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=state.sourcePandaStatesMonoTime)
pandas.pandaStates[0] = sm['pandaStates'][0]
setattr(pandas.pandaStates[0], field, value)
sm.update_msgs(NOW / 1e9, [pandas.as_reader()])
assert current(sm, cp) == state
assert not transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
def test_cache_hit_uses_exact_original_inclusive_deadline():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
assert current(sm, cp) == state
assert current(sm, cp, state.validUntilMonoTime) == state
assert profile.call_count == 1
assert current(sm, cp, state.validUntilMonoTime + 1) is None
assert sm._aol_native_admission is None
def test_invalid_profile_keeps_original_short_circuit_with_missing_owner_stamp():
cp = configured_cp()
state = bound_state(cp)
sm = sm_for(cp, state)
cp.radarUnavailable = True
sm.logMonoTime.pop('aolSafetyWire')
assert current(sm, cp) is None
+8
View File
@@ -38,6 +38,14 @@ def native_matches_cp(CP, model: int, param: int) -> bool:
return False
def native_admission_key(CP):
"""Observation reuse is scoped to the exact Ioniq single-Panda contract."""
try:
return hyundai.native_admission_key(CP) if CP.brand == 'hyundai' else None
except (AttributeError, TypeError, ValueError, OverflowError, IndexError):
return None
def native_latch_rejected(CP, native, *, state=None) -> bool:
if native is None:
return False
+14
View File
@@ -73,6 +73,20 @@ def qualified_ioniq6(CP) -> bool:
return raw == (0x8895 if flags & int(HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) else 0x8815)
def native_admission_key(CP):
"""Complete mutable CP inputs to exact one-slot Ioniq native admission.
Keep this in sync with qualified_ioniq6 and native inventory qualification.
This key is not an admission decision and never contains live authority.
"""
if CP.brand != 'hyundai' or CP.carFingerprint != HyundaiCAR.HYUNDAI_IONIQ_6 or len(CP.safetyConfigs) != 1:
return None
return (str(CP.brand), str(CP.carFingerprint), int(CP.flags), bool(CP.radarUnavailable),
bool(CP.notCar), bool(CP.passive), bool(CP.dashcamOnly),
bool(CP.openpilotLongitudinalControl), bool(CP.pcmCruise), int(CP.alternativeExperience),
tuple((int(c.safetyModel.raw), int(c.safetyParam)) for c in CP.safetyConfigs))
def ioniq6_settings_capable(CP) -> bool:
# Stock ACC lateral settings are independent of the experimental LONG opt-in.
return _stock_ioniq6(CP) or (IONIQ6_LONG_PREARM_ENABLED and ioniq6_long_eligible(CP))