Skip to content

rewrite(phase2): port e2e/lifecycle from c-RCP, correct HARA.md's overstated H-004 claim - #134

Merged
SoundMatt merged 1 commit into
rewrite/v3-from-c-rcpfrom
phase2/e2e-lifecycle
Aug 21, 2026
Merged

SoundMatt merged 1 commit into
rewrite/v3-from-c-rcpfrom
phase2/e2e-lifecycle

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Part of the v3.0.0 ground-up rewrite (#129, ROADMAP.md Phase 17). Second and final batch of Phase 2 — the safety-critical core: CRC32 safe points, safety-request handling, replay/staleness detection, and the RC-Server lifecycle state machine.

Real content deltas found and fixed

  • CRC coverage was missing 3 leading bytes (c-RCP issue #465): avtp_subtype/header_octet1/tu — TC18 Figures 20/21's own "header CRC" region — were absent entirely. A conformant peer's CRC32 would never have matched this library's prior output.
  • wrap()/unwrap() didn't exist at all — only append_crc() existed, producing the wrong wire order whenever a payload carries trailing pad bytes (c-RCP issue #420).
  • RxSequenceGuard was substantively wrong, not just incomplete: compared sequence numbers as a non-wrapping uint32_t (spurious rejection on every 8-bit sequence_num wrap) and never consulted rx_seq_safestate_enable at all. Replaced with c-RCP's RFC 1982 forward-window comparison.
  • endpoint_in_configured_safe_state() was missing REQ-SEQ-012's fail-closed rule.
  • Missing entirely, now ported: fragment_carries_crc()/compute_fragmented_crc(), crc_error_should_enter_safe_state(), StreamFaultTracker, StreamStatus, length_with_crc()/data_length_for_protected_members(), overflow_should_enter_safe_state().

lifecycle.hpp had only a bare 3-state machine — c-RCP's whole access-control layer (writer authorization, unicast enforcement, idle-gating, issue #198) was absent. Added, all additive: real TC18 §12.3.1.2 plausibility content, ServerLifecycle::transition() (full writer-auth + idle-gating state machine), Disposition/should_accept(), register-locking-by-state. advance() kept unchanged (existing dependents rely on its documented same-state semantics); transition() coexists as the richer entry point.

HARA.md/FORMAL_VERIFICATION.md correction

HARA.md's H-004 section described RxSequenceGuard::check in present-tense mitigating language with no caveat. Added a "STATUS CORRECTED" block: the mechanism is implemented and unit-tested but never instantiated outside its own test — citing c-RCP's #601/#606 resolution as precedent, stating H-004 is "effectively Open" pending dispatch wiring (Phase 4 scope — mock.hpp was not touched here). FORMAL_VERIFICATION.md got the same caveat plus an independently-found second issue: tla/RxSequenceGuard.tla itself still models the old non-wrapping comparison — flagged as a tracked follow-up.

Fixed-capacity

StreamFaultTracker: std::array<Slot, 16>, no heap allocation, bound from c-RCP's RCP_E2E_STREAM_FAULT_TRACKER_MAX_STREAMS. lifecycle.c confirmed to have no heap dependency.

Left as TODO(phase2-followup)

apply_acf_length_adjustment()'s 9-bit overflow fail-safe (low exposure, would touch every call site); RxSequenceGuard.tla's staleness (separate formal-modeling task).

test_e2e.cpp: 32→72 cases. test_lifecycle.cpp: 16→47 cases.

Verified

Full tree rebuilds clean (223/223 targets, incl. the merged watchdog/alloc-seam batch), 56/56 ctest suites pass (100%) — independently re-verified from a fresh build.

Tracked at #129 — completes Phase 2.

…rstated H-004 claim

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second and final batch of Phase 2 -- the
safety-critical core: CRC32 safe points, safety-request handling,
replay/staleness detection, and the RC-Server lifecycle state machine.

Real content deltas found and fixed against c-RCP's e2e.h/e2e.c and
lifecycle.h/lifecycle.c:

- CRC coverage was missing 3 leading bytes (c-RCP issue #465):
  avtp_subtype/header_octet1/tu -- TC18 Figures 20/21's own 'header
  CRC' region -- were absent entirely from coverage_buffer()/
  compute_crc()/verify_crc(). A conformant peer's CRC32 would never
  have matched this library's prior output.
- wrap()/unwrap() didn't exist at all -- only append_crc() (appends at
  buffer end), which produces the wrong wire order
  [header][real][pad][CRC] whenever a payload carries trailing pad
  bytes; TC18 requires [header][real][CRC][pad] (c-RCP issue #420).
- RxSequenceGuard (REQ-E2E-028/029, the mechanism a prior audit this
  session flagged as implemented-but-unwired) was substantively wrong,
  not just incomplete: compared sequence numbers as a non-wrapping
  uint32_t (would spuriously reject every request once an 8-bit AVTPDU
  sequence_num wraps) and never consulted rx_seq_safestate_enable at
  all. Replaced with c-RCP's RFC 1982 forward-window comparison.
- endpoint_in_configured_safe_state() was missing REQ-SEQ-012's
  fail-closed rule -- a disabled sequencer (state==0) could satisfy a
  safe-state check if rx_safe_sequencer_state was also misconfigured
  to 0.
- Missing entirely, now ported: fragment_carries_crc()/
  compute_fragmented_crc() (REQ-E2E-010/038), crc_error_should_enter_
  safe_state() (REQ-E2E-045), StreamFaultTracker (REQ-E2E-021),
  StreamStatus (REQ-E2E-046), length_with_crc()/
  data_length_for_protected_members() (REQ-E2E-004/037),
  overflow_should_enter_safe_state() (REQ-E2E-030).

lifecycle.hpp had only a bare 3-state machine with no real
access-control layer -- c-RCP's whole issue #198 (writer authorization,
unicast enforcement, idle-gating) was absent. Added, additive:
EndpointPlausibility/RequestStreamPlausibility/PlausibilitySnapshot +
check_hw_cfg()/check_rcp_cfg() (real TC18 Sec12.3.1.2 plausibility
content, not just an opaque callback); WriterCtx;
ServerLifecycle::transition() (full writer-authorization + idle-gating
state machine, including the RCP_CONFIGURED->HW_CONFIGURED demotion
and both authorized resets advance() never modeled); Disposition/
should_accept() (per-state request filtering); field_writable()/
field_write_error() family (register-locking-by-state). advance()
kept unchanged (mock.hpp and existing tests depend on its differing
same-state semantics, a documented pre-existing design choice);
transition() coexists as the richer, spec-complete entry point.

HARA.md/FORMAL_VERIFICATION.md correction: HARA.md's H-004 section
described RxSequenceGuard::check in present-tense mitigating language
with SG-004 listed as a clean ASIL-B mitigation and no caveat. Added a
'STATUS CORRECTED' block: the mechanism is implemented and unit-tested
but never instantiated outside its own test -- not in mock::Server's
dispatch or any transport Server -- citing c-RCP's #601/#606
resolution as the parallel precedent, stating H-004 is 'effectively
Open' pending that wiring (Phase 4/server-dispatch scope, not this
pass -- mock.hpp was NOT touched, RxSequenceGuard is referenced only
in e2e.hpp itself). FORMAL_VERIFICATION.md got the same caveat, plus a
second, independently-found correction: tla/RxSequenceGuard.tla itself
still models the old non-wrapping comparison, not the RFC 1982
algorithm now in e2e.hpp -- flagged as a real, un-re-derived
divergence and tracked as a follow-up (re-deriving the TLA+ spec is
its own formal-modeling task).

Fixed-capacity: StreamFaultTracker uses a std::array<Slot, 16>, no
heap allocation -- bound taken directly from c-RCP's
RCP_E2E_STREAM_FAULT_TRACKER_MAX_STREAMS. lifecycle.c confirmed to
have no alloc.h/heap dependency; no fixed-capacity work needed there.

Left as // TODO(phase2-followup): apply_acf_length_adjustment() doesn't
fail-safe on 9-bit field overflow the way c-RCP's
adapt_acf_msg_length() does (low real exposure, but a fallible return
type would touch every existing call site) -- named gap, not silently
dropped. RxSequenceGuard.tla's staleness (above) is a second, separate
tracked follow-up.

test_e2e.cpp: 32 -> 72 TEST_CASEs. test_lifecycle.cpp: 16 -> 47
TEST_CASEs.

Verified: full tree rebuilds clean (223/223 targets, incl. the merged
watchdog/alloc-seam batch), 56/56 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report. No call sites outside target files required
changes (regmap.hpp/mock.hpp depend on lifecycle.hpp, watchdog.hpp
depends on e2e.hpp, all compile and pass unchanged since every new
piece is additive).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit 692f017 into rewrite/v3-from-c-rcp Aug 21, 2026
22 checks passed
@SoundMatt
SoundMatt deleted the phase2/e2e-lifecycle branch August 21, 2026 20:10
SoundMatt added a commit that referenced this pull request Aug 22, 2026
…Tracker, RxWatchdog (#148)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch C of a four-batch mock.hpp
port, built on batch A (admission) and batch B (response suppression +
regmap/discovery wiring).

This closes the exact gap flagged during Phase 2 (PR #134, e2e.hpp's
own header comment): RxSequenceGuard, StreamFaultTracker, and
RxWatchdog were all fully implemented in e2e.hpp but never called from
any dispatch path anywhere in cpp-RCP -- e2e.hpp's own comment
explicitly deferred this wiring to "Phase 4 (server/dispatch) scope,
matching c-RCP's own mock.c wiring (frame_seq_gate_admits(),
dispatch_frame_e2e())." That deferred work lands here.

## Ported (c-RCP src/mock.c -> Server)

- rcp_mock_server_dispatch_e2e() (src/mock.c:1892-2038, REQ-E2E-021/
  031/041, REQ-WDG-010) -> new Server::dispatch_e2e().
- frame_seq_gate_admits() (src/mock.c:3038-3063, REQ-E2E-028/029) ->
  private Server::seq_gate_admits(), folded into single-member
  dispatch per this batch's scope (multi-member AVTPDU frame splitting
  is batch D's job; this batch's guard logic is built at the
  single-member level so batch D can reuse the same guard instance).
- rcp_mock_server_check_watchdog() (src/mock.c:991-1021, REQ-E2E-046)
  -> public Server::check_watchdog_overflow().
- rcp_mock_server_stream_status_rx_blocked() (src/mock.c:981-989) ->
  Server::stream_rx_blocked().
- Watchdog-kick call sites (REQ-WDG-010) -> unconditional
  rx_watchdog_kick() at the top of dispatch_e2e().

## Shape of the new E2E dispatch surface

One public Server::dispatch_e2e() entry point, not ten
dispatch_<type>_e2e() siblings. ep_req_crc_enable (TC18 Sec12.7.1) is a
per-endpoint, not per-endpoint-type, toggle, and c-RCP's own
rcp_mock_server_dispatch_e2e() is likewise a single function
parameterized by byte_bus_id, not duplicated per handler -- mirrors
dispatch()'s own established single-entry-point convention. All 10
operational endpoint types get E2E support uniformly via
regs_.generic_configs[].ep_req_crc_enable (the real register). EP0
gets none -- it has no generic_configs[] row, matching c-RCP's own
EP0 exclusion from admission entirely.

## New Server state

e2e::StreamFaultTracker stream_fault_tracker_;
std::array<e2e::RxSequenceGuard/RxWatchdog/StreamStatus, kMaxEntries=64>
keyed by resolved request_streams[] index (mirrors c-RCP's
srv->seq_tracker[]/srv->stream_status[]) -- all owned directly, matching
mock.hpp's existing convention over c-RCP's pointer/setter style.

## Deliberate scope boundaries (documented in-file, not TODO-stubbed --
none block correctness)

- REQ-E2E-045/029's full stream-wide "broadcast safe state to every
  bound endpoint" actuation is not implemented (no SequencerTable-
  driving actuator exists in this file yet) -- only the observable
  StreamStatus latch is. Pre-existing gap class shared with batch A/B's
  own admission-reject overflow-safestate consequence, not new here.
- check_watchdog_overflow()'s purge targets server::Endpoint::
  watchdog_purge() (the pending/conditional store) rather than
  rcp::watchdog::Manager (RequestLedger-keyed, architecturally
  mismatched to this mock's admission storage).
- No TSCF variant of dispatch_e2e (c-RCP's dispatch_e2e_tscf, REQ-
  TIMED-012/013) -- out of scope since plain dispatch() has no TSCF
  timing gate either yet.

No content bugs found versus c-RCP. One test-fixture-only issue found
while writing coverage: GPIO's apply_gpio_write() masks writes by pin
direction (default all-input, TC18 Sec13.7.4.3) -- tests needed a
configure_gpio_all_outputs() fixture helper before asserting OR-write
effects; not a library bug.

## Verification

New Server::dispatch_e2e() + 6 new TEST_CASEs covering replay
detection, stream-fault short-circuit, and watchdog kick/overflow,
integration-tested via mock.hpp's dispatch (not re-testing e2e.hpp's
own already-unit-tested internals).

include/rcp/mock.hpp: 1759 -> 2162 lines. tests/test_mock.cpp: 1561 ->
1832 lines, 71 -> 77 TEST_CASEs (632 assertions). include/rcp/e2e.hpp:
header comment updated to record the closed deferral (primitives
unchanged). Full tree rebuilds clean (0 errors, 0 warnings, verified
with -Wall -Wextra), 58/58 ctest suites pass (100%) -- independently
re-verified from a fresh build, not just the porting agent's own
report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
SoundMatt added a commit that referenced this pull request Aug 22, 2026
…ses out Phase 4 (#150)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second of two sub-batches (D1/D2) making
up Phase 4's fourth and FINAL batch. This completes mock.hpp -- the
last module of Phase 4 -- and closes out the server/dispatch layer in
full. Built on batches A (admission), B (response suppression +
regmap/discovery), C (single-member E2E dispatch), and D1 (E2E
fragmented dispatch).

## Ported (c-RCP src/mock.c/src/scheduler.c/src/request.c, REQ-MOCK-019/
020/029, REQ-E2E-033, REQ-CANCEL-012)

- split_frame_members() -- ported from rcp_sched_split_frame_members()
  (scheduler.c:63-106), a new namespace-scope helper (request.hpp's own
  header explicitly notes this splitting logic lives nowhere else in
  the rewrite -- c-RCP keeps it mock.c-local too).
- FrameMemberResult + Server::dispatch_frame()/dispatch_frame_e2e() --
  ported from rcp_mock_server_dispatch_frame()/_dispatch_frame_e2e()
  (mock.c:3068-3382). Each returns per-member results (mirrors c-RCP's
  own result-array shape -- c-RCP does not aggregate members' responses
  into one outgoing wire frame either, confirmed by source read).
- Chain-group/position bookkeeping (REQ-CANCEL-012, ported from
  rcp_chained_advance/is_chained_member/last_pending_index,
  mock.c:2950-3231, request.c:673-694) -- via a new shared
  dispatch_frame_impl() template, using request::should_execute_chained()
  for the cs-polarity predicate and a new last_pending_index() helper.
- apply_cancellation()'s ClearSingle TODO (open since batch A) filled
  in: decodes request::decode_clear_single() from the raw frame,
  captures the target's chain_group/chain_position before removing it,
  cascades via cancel_chain_from().

## Bug found and fixed (beyond the literal ask)

admit_and_classify() always re-encoded ABB from req/payload, silently
stripping any genuine GBB conditional/cancellation opcode's semantics
-- meaning ClearSingle/Chained frames were never actually reachable
before this batch, regardless of how they'd been decoded. Refactored
to take the raw frame directly (mechanical edit across its 10 call
sites, zero behavior change for them) and added
decode_and_dispatch()'s own conditional-opcode-aware routing branch, so
dispatch_frame()/dispatch_frame_e2e() -- and every existing
dispatch_e2e()/dispatch_e2e_fragment() caller -- can now genuinely
admit Chained/Compound/Triggered/Timed/Cancellation GBB members. Also
extracted dispatch_e2e_core()/dispatch_e2e_fragment_core() with an
apply_seq_gate flag so the frame-level sequence gate runs exactly once
per frame (matching the pre-existing doc comment's own stated intent),
not once per member.

## Delta from c-RCP (documented in-file)

dispatch_frame_e2e() routes every member through
dispatch_e2e_fragment_core() rather than plain dispatch_e2e_core() --
c-RCP has no frame-level fragment-aware dispatch at all; this is a
safe superset since the fragment-core function falls back identically
for non-fragmented members.

## Test-writing findings (documented, not library bugs)

split_frame_members() requires each member's declared
acf_msg_length*4 to exactly match its real encoded byte count, but
acf::encode_acf_abb()/e2e::wrap_framed() never auto-pad payloads to
quadlet boundaries (documented as caller-owned in acf.hpp) -- never
mattered for single-member dispatch before; all new multi-member test
frames use quadlet-aligned payloads with an explanatory comment.
Confirmed DispatchErrc::unknown_bus is unreachable in this codebase
(acf.hpp's decode is deliberately lenient vs c-RCP's strict decode) --
documented as defensive-only with a test pinning the actual lenient
behavior.

## Final Phase-4 sweep

Grepped the whole file for TODO(phase4-batch-*) markers: only
apply_cancellation()'s ClearSingle stub existed, now resolved. No
dangling phase-4 markers remain anywhere in mock.hpp.

## Verification

New split_frame_members()/dispatch_frame()/dispatch_frame_e2e() +
ClearSingle cascade, tested end-to-end via multi-member AVTPDU frames.
include/rcp/mock.hpp: 2723 -> 3413 lines. tests/test_mock.cpp: 2349 ->
2775 lines, test_mock: 101 TEST_CASEs (863 assertions). Full tree
rebuilds clean (0 errors, 0 warnings), 58/58 ctest suites pass (100%)
-- independently re-verified from a fresh build, not just the porting
agent's own report.

## Phase 4 complete

mock.hpp grew from its pre-Phase-4 baseline of 917 lines to 3413 lines
across four batches: A added server::Endpoint admission wiring, B
added Table 24 response suppression plus regmap/discovery storage, C
wired RxSequenceGuard/StreamFaultTracker/RxWatchdog into single-member
E2E dispatch (closing a gap flagged since PR #134), D1 wired
fragment.hpp/respqueue.hpp for E2E fragmented dispatch (closing a gap
open since Phase 1), and this D2 batch added AVTPDU frame-level
multi-member splitting/routing/chain-cascade cancellation. Combined
with regmap (batches A/B), the new server.hpp, discovery, and adapt --
all merged earlier in Phase 4 -- the server/dispatch layer is now
complete with no phase-4-scoped work left deferred.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant