Skip to content

fix: v2.22.0 — ACF_GBB message_timestamp is spliced between the Message Info header's two quadlets - #100

Merged
SoundMatt merged 1 commit into
mainfrom
fix/acf-gbb-timestamp-splice
Aug 1, 2026
Merged

SoundMatt merged 1 commit into
mainfrom
fix/acf-gbb-timestamp-splice

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

The bug

encode_acf_gbb/decode_acf_gbb placed the 64-bit message_timestamp after the complete 8-byte shared Message Info header, at wire octet 8. TC18 splices it between that header's two 4-byte quadlets, at wire octet 4 — which pushes the entire second quadlet (evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size_or_segment_num) from octet 8 to octet 12.

octets correct (this PR) previous (wrong)
0..3 quadlet 0 — acf_msg_type/acf_msg_length/pad/mtv/rsv/byte_bus_id quadlet 0 (same)
4..7 message_timestamp[63:32] quadlet 1
8..11 message_timestamp[31:0] message_timestamp[63:32]
12..15 quadlet 1 — evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size message_timestamp[31:0]
16.. byte_msg_payload byte_msg_payload (same)

The total message length was unaffected, the payload started at the same offset, and the codec round-tripped against its own decoder perfectly. Only the four bytes in the middle were in the wrong place — which is exactly why neither the v2.19.0 bit-packing re-derivation nor the v2.20.0 acf_msg_length pass caught it: both were auditing bit positions within the header and the length field, not the header's byte geometry around the timestamp.

Consequences: every ACF_GBB message this library has ever emitted was unreadable by a conformant peer (and vice versa), and every ACF_GBB E2E CRC was computed over the wrong byte sequence. ACF_ABB is entirely unaffected — it has no timestamp field, so its two quadlets have always been correctly contiguous at octets 0..7.

How the correct layout was verified

Independently, and before any code changed, against the specification PDF itself — the relevant layouts exist only as figures, not as extractable text, so the pages were rendered at high DPI and read directly rather than trusting a text extraction or this codec's prior behavior. Four mutually independent confirmations:

  1. The single-ACF_GBB CRC-coverage figure draws one "Byte Message Info" group of three rows in this order: the acf_msg_type/acf_msg_length/pad/mtv/rsv/byte_bus_id quadlet; then message_time_stamp as a double-height 64-bit block (rendered exactly the way that same figure's 64-bit stream_id is drawn); then the evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size quadlet.
  2. Arithmetic cross-check from that same figure, which states acf_msg_length = 0x07 for its worked example: 7 quadlets = 28 octets = quadlet0(4) + timestamp(8) + quadlet1(4) + payload(7 real + 1 pad = 8) + CRC32(4). No other placement of a 64-bit timestamp closes that sum. Its ACF_ABB counterpart states 0x05 = 20 octets = 4 + 4 + (6 real + 2 pad = 8) + 4, confirming the same two quadlets with nothing between them.
  3. The compound-request figure (an ACF_GBB with mtv=0) shows the fields that repurpose the timestamp slot — request_type/cmp_start_state/cmp_next_state/cmp_sequencer, then cmp_exec_delay/cmp_repetitions — occupying exactly the two quadlets between the same two header quadlets, i.e. octets 4..11. That figure also independently confirms rcp/request.hpp's existing byte assignment within the slot (opcode in the slot's first octet, seven parameter octets after), which needed no change.
  4. The response-field table lists the Message Info fields in wire order and places message_timestamp ("Present in ACF_GBB, omitted in ACF_ABB") between byte_bus_id — quadlet 0's last field — and evt — quadlet 1's first field.

What changed

  • include/rcp/acf.hpp — new kAcfHeaderQuadletLen / kAcfGbbTimestampOffset / kAcfGbbSecondQuadletOffset / kAcfGbbMessageInfoLen, documented with the point-by-point verification above, plus encode_acf_gbb_message_info / decode_acf_gbb_message_info. These own the ACF_GBB-specific byte geometry and delegate all bit-level packing to the existing encode_acf_message_info / decode_acf_message_info, so the bit layout keeps exactly one definition in the file. encode_acf_gbb, decode_acf_gbb and the multi-message walker decode_acf_messages all route through them.
  • include/rcp/e2e.hpp — coverage_buffer serialized the ACF_GBB Message Info block by hand (header, then timestamp) and inherited the same wrong order. It now calls acf::encode_acf_gbb_message_info, making the CRC-covered bytes byte-identical to the transmitted bytes by construction rather than by two code paths happening to agree. A new assertion pins that identity directly.
  • include/rcp/udp.hpp, include/rcp/l2.hpp, include/rcp/record.hpp — no change needed. All three already route every ACF_GBB message through encode_acf_gbb/decode_acf_gbb and hardcode no offsets of their own; grepped and confirmed. They inherit the fix.
  • include/rcp/request.hpp — no functional change needed either: it deals only in the value of the 64-bit slot, never its wire position. Gained a comment recording that, plus confirmation (3) above of its within-slot byte assignment.
  • .fusa-reqs.json — REQ-WIRE-005 and REQ-E2E-002 now state the exact octet positions rather than leaving the geometry implicit. (REQ-E2E-002 also still said "the 10-byte ACF shared header", stale since v2.19.0 made it 8.) REQ-SEQ-001 records where the repurposed slot actually sits.
  • ROADMAP.md / README.md — new milestone 66 with the full derivation; GA renumbered 66 → 67; the summary table gains rows for v2.21.0 (previously missing) and v2.22.0; milestone 63's stale "at the correct byte offset" claim about CRC coverage corrected in place.

Tests

tests/test_acf.cpp and tests/test_e2e.cpp had pinned the bug as correct — the classic tautology failure of asserting an encoder against its own output. Rewritten around literal octet positions derived from the figures above, with the derivation written out in the test files themselves:

  • test_acf.cpp's layout comment now documents both message types' octet positions and the arithmetic cross-check, and the ACF_GBB byte-sequence case asserts one whole hand-written literal frame, then re-asserts field by field at literal offsets, then decodes the literal back to fields — the direction that proves the decoder handles spec-shaped bytes rather than merely its own encoder's bytes. Two explicit regression assertions (frame[4] == 0x11, frame[8] == 0x55) discriminate the two layouts directly.
  • test_e2e.cpp gains a full-message conformance vector reproducing the CRC-coverage figure's own message field for field (acf_msg_type 0x0D, acf_msg_length 0x07, pad 1, 7+1-octet payload, CRC32 trailer, 28 octets total), asserted octet for octet in both directions; and its coverage_buffer case now asserts the covered bytes equal encode_acf_gbb's output exactly.

Mutation-checked: reverting the two offset constants to their pre-fix values (4 → 8, 12 → 4) fails 4 test cases across the two suites. The new expectations genuinely discriminate between the two layouts rather than restating whatever the encoder does.

Full local build clean, ctest 53/53 suites pass (macOS/arm64, GCC 16, C++17). The l2-veth-roundtrip program is Linux-only (AF_PACKET) and was not runnable in this environment; its CI job covers it. The UDP round-trip suite (rcp_udp) runs and passes here, exercising the GBB path end to end over real sockets.

Breaking change

This changes bytes on the wire for every ACF_GBB message, so a v2.22.0 peer cannot interoperate with a pre-v2.22.0 peer for GBB traffic. ACF_ABB traffic is unaffected. No public API signature, struct field, or function name changes — AcfMessageInfo and the encode_acf_gbb/decode_acf_gbb signatures are identical — so this is source-compatible and wire-incompatible.

Since the pre-v2.22.0 bytes were never conformant with anything but themselves, correcting them is strictly a move toward the interoperability GA requires. kVersion/CMakeLists.txt bump 2.21.0 → 2.22.0, following the same minor-bump convention v2.19.0, v2.20.0 and v2.21.0 used for their own wire-format corrections during this pre-GA v2.x line.

…r quadlets

encode_acf_gbb/decode_acf_gbb placed the 64-bit message_timestamp after the
complete 8-byte shared Message Info header (wire octet 8). TC18 splices it
between that header's two 4-byte quadlets (wire octet 4), which pushes
evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size_or_segment_num from
octet 8 to octet 12. Total message length was unaffected, and this codec
round-tripped against itself perfectly, which is why neither the v2.19.0
bit-packing pass nor the v2.20.0 acf_msg_length pass caught it — but every
ACF_GBB message this library emitted was unreadable by a conformant peer,
and every ACF_GBB E2E CRC was computed over the wrong byte sequence.
ACF_ABB is unaffected: it has no timestamp field.

Verified against the specification PDF's own figures (rendered at high DPI
and read directly, not inferred from this codec) four independent ways:
the single-ACF_GBB CRC-coverage figure's three-row "Byte Message Info"
group (quadlet 0, 64-bit message_time_stamp, quadlet 1); that figure's own
stated acf_msg_length 0x07 = 28 octets, which only closes as
4 + 8 + 4 + 8 + 4 (its ACF_ABB counterpart states 0x05 = 20 = 4 + 4 + 8 + 4);
the compound-request figure, where the mtv=0 repurposing fields occupy
octets 4..11 between the same two quadlets; and the response-field table,
which lists message_timestamp between byte_bus_id and evt.

- acf.hpp: new kAcfHeaderQuadletLen/kAcfGbbTimestampOffset/
  kAcfGbbSecondQuadletOffset/kAcfGbbMessageInfoLen and
  encode_acf_gbb_message_info/decode_acf_gbb_message_info, which own the
  spliced byte geometry and delegate all bit packing to the existing
  encode/decode_acf_message_info. encode_acf_gbb, decode_acf_gbb and
  decode_acf_messages route through them.
- e2e.hpp: coverage_buffer hand-serialized the same wrong order; it now
  calls acf::encode_acf_gbb_message_info, so the CRC-covered bytes equal
  the transmitted bytes by construction.
- udp.hpp/l2.hpp/record.hpp needed no change (all route through the codec,
  no hardcoded offsets). request.hpp needed none either (it deals in the
  slot's value, not its position) and gained only a comment.
- tests: test_acf.cpp and test_e2e.cpp had pinned the bug as correct by
  reading back the encoder's own output. Rewritten around literal
  spec-derived octet positions with the derivation spelled out, plus a new
  full-message conformance vector reproducing the CRC-coverage figure's
  message octet for octet and asserted in both directions. Mutation-checked:
  restoring the old offsets fails 4 test cases.
- .fusa-reqs.json: REQ-WIRE-005/REQ-E2E-002 now state exact octet positions
  (REQ-E2E-002 also still said "10-byte ACF shared header", stale since
  v2.19.0 made it 8); REQ-SEQ-001 records where the repurposed slot sits.

Wire-format-breaking for ACF_GBB only; no public API signature changes.
kVersion/CMakeLists.txt 2.21.0 -> 2.22.0. Build clean, ctest 53/53.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit 7facb88 into main Aug 1, 2026
22 checks passed
@SoundMatt
SoundMatt deleted the fix/acf-gbb-timestamp-splice branch August 1, 2026 04:05
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