Skip to content

cell M6: handover command + beacon IE + TSF continuity (F1 v4) - #529

Closed
josephnef wants to merge 2 commits into
masterfrom
feat/m6-ho-core
Closed

josephnef wants to merge 2 commits into
masterfrom
feat/m6-ho-core

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Part of M6 (#260), epic #254. Step 2a, headless: the pieces that move a UE's schedule from cell A to cell B. The DU/UE execution and the rig test come next.

  • src/cell/HoCommand.h:
    • HoCommand (CU → both DUs): the UE, its id and position at the target, the network-wide switch superframe, and the TSF delta.
    • ho_tsf_delta_us: for DUs on the PTP fabric, tsf_k(t) = t − phase_k − net_off_k·P, so the target's clock is the source's plus (phase_s − phase_t) + (net_off_s − net_off_t)·P. A UE locked to A therefore knows B's clock exactly, with no scan and no re-acquisition.
    • HoIe (A's beacon → the UE): carries a CRC and the cell key's HMAC tag, so a forged handover is inert. find_ho_ie returns the newest command addressed to that UE.
  • F1 v4: Type::Handover with a freshness seq; Report.net_off, filled by the DU; and an Ack.kind so handover and schedule acks are distinct.
  • cell_m6 ctest:
    • the TSF delta on a simulated two-DU timeline;
    • the handover message round trip, tags and every truncation;
    • the IE inside an MPDU: newest id wins, a wrong key is inert, a torn IE is dropped, unkeyed vs keyed.
  • Checks: ctest 92/92, and the M5 rig run passes on F1 v4.

🤖 Generated with Claude Code

src/cell/HoCommand.h:
- HoCommand is what the CU sends both DUs: the UE, source and target,
  its id and position at the target, the network superframe of the switch,
  and the TSF delta.
- ho_tsf_delta_us is that delta: for DUs on the PTP fabric,
  tsf_k(t) = t - phase_k - net_off_k * P, so target - source =
  (phase_s - phase_t) + (net_off_s - net_off_t) * P. A UE locked to the
  source knows the target's clock exactly: no scan, no re-acquisition.
- HoIe is what the source DU tells the UE in its beacon. It carries a CRC
  and the cell key's tag (label 'H'), so a forged handover is inert;
  find_ho_ie returns the newest id addressed to (cell, UE).

F1 v4:
- Type::Handover (encode/decode_handover, with the freshness seq);
- Report carries net_off, filled by the DU;
- Ack carries its kind (schedule / handover), and the CU's schedule
  re-issue only reacts to schedule acks.

The cell_m6 ctest covers:
- the TSF delta against a simulated two-DU timeline (and both grids on
  network superframe boundaries);
- the F1 handover round trip, tags and every truncation, and the Ack
  kind;
- the IE in an MPDU: newest id, wrong key inert, a torn IE dropped while
  the intact one stays, unkeyed vs keyed.
ctest 92/92. Rig: the M5 coordination run passes on F1 v4.

Refs #260

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Define F1 handover commands and authenticated beacon IEs

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Defines handover commands and TSF offsets for a planned network-superframe switch.
• Extends F1 reports and acknowledgments to carry handover timing and distinguish message types.
• Adds authenticated beacon IE parsing and tests for timing, wire integrity, and key handling.
Diagram

sequenceDiagram
    participant S as Source DU
    participant C as Controller
    participant T as Target DU
    actor U as UE
    S->>C: F1 v4 report with offset
    T->>C: F1 v4 report with offset
    Note over C,T: Command transport defined; execution follows
    C-->>S: Handover command format
    C-->>T: Handover command format
    S-->>U: Beacon Ho IE format
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Embed handover in the grant-map IE
  • ➕ Could reuse the existing beacon IE and its parsing path.
  • ➖ Couples a one-time handover command to recurring grants.
  • ➖ The existing grant-map IE has a CRC but not the handover IE's keyed authentication.

Recommendation: Keep the separate authenticated handover IE and F1 command: they carry switch-specific identity and timing without changing grant-map semantics. Review the future execution work against these formats, since this PR defines transport but does not yet perform a handover.

Files changed (6) +338 / -10

Enhancement (3) +251 / -5
main.cppPopulate DU timing offset and identify schedule acknowledgments +3/-1

Populate DU timing offset and identify schedule acknowledgments

• Adds the DU's network-superframe offset to F1 reports when available. Marks existing schedule acknowledgments with the new Schedule kind.

examples/cell/main.cpp

F1.hExtend F1 to version 4 for handover transport +63/-4

Extend F1 to version 4 for handover transport

• Adds an authenticated Handover message codec and a network-superframe offset to reports. Adds an acknowledgment kind so schedule and handover responses can be distinguished.

src/cell/F1.h

HoCommand.hDefine handover timing, commands, and beacon IE codecs +185/-0

Define handover timing, commands, and beacon IE codecs

• Introduces the source-to-target TSF delta calculation and the command fields needed for a scheduled switch. Adds CRC-checked, optionally keyed handover IE encoding and parsing that selects the newest valid command for a UE.

src/cell/HoCommand.h

Bug fix (1) +2 / -1
main.cppRestrict schedule re-issue to schedule acknowledgments +2/-1

Restrict schedule re-issue to schedule acknowledgments

• Prevents a failed handover acknowledgment from clearing a schedule whose version happens to match the handover ID.

tools/ran-controller/main.cpp

Tests (2) +85 / -4
cell_m5_selftest.cppAdapt existing F1 round trips to version 4 fields +3/-2

Adapt existing F1 round trips to version 4 fields

• Exercises report round trips with a signed network offset and updates acknowledgment fixtures and byte positions for the new kind field.

tests/cell_m5_selftest.cpp

cell_m6_selftest.cppTest TSF continuity and handover wire formats +82/-2

Test TSF continuity and handover wire formats

• Checks the TSF delta against two simulated DU clocks, including network-superframe boundaries. Tests handover F1 round trips and truncations, plus beacon IE selection, corruption, and keyed versus unkeyed behavior.

tests/cell_m6_selftest.cpp

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Old beacons can replay a stale handover ✓ Resolved

Resolving commit: 8ea6edc

Resolution reason
Adds a HoReceiver class on the UE side that accepts an IE only if its id is strictly above every
previously accepted id and its effective_sf is still ahead of the UE's current source superframe, so
replayed or superseded IEs are rejected. Also restricts find_ho_ie to beacon frames (mpdu[0]==0x80)
and documents that cross-beacon replay is the caller's to reject. Tests cover replay, superseded id,
and spent instant cases. This uses the existing id and effective_sf rather than adding a new
freshness field to the encoded body, a different but valid implementation.
Description
HoIe carries a monotone id and a CRC/HMAC tag but no freshness sequence, and nothing in this
diff records the last accepted handover id per UE. find_ho_ie only picks the newest id within a
single MPDU, so an older, validly tagged beacon captured earlier can be replayed later and
re-present a superseded handover to the UE. The F1 Handover message does carry seq, so replay
protection exists on the CU-to-DU leg but not on the over-the-air IE that the UE acts on.

2. Non-beacon frames pass handover lookup ✓ Resolved

Resolving commit: 8ea6edc

Resolution reason
Added a frame-control check in find_ho_ie: mpdu[0] != 0x80 (beacons only) now returns nullopt
before scanning IEs. Added a self-test that sets a data-frame control byte (0x08) on the same bytes
and asserts find_ho_ie rejects it, and sets m2[0]=0x80 for the beacon case so it still passes. This
is the suggested fix (frame-control check plus test with a non-beacon frame carrying a valid IE at
offset 36).
Description
find_ho_ie starts scanning at the beacon IE offset without checking that the MPDU is a beacon. If
a caller passes a non-beacon frame containing a valid handover IE at that offset, the function
returns it as a handover command.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

- HoReceiver is the UE side. It acts on a handover IE only if its id is
  above every one it accepted and its switch instant (covered by the tag)
  is still ahead, so a captured beacon replayed later is inert.
- find_ho_ie scans beacons only (frame control 0x80).
- selftest: replay of the same, a superseded and a spent IE; a non-beacon
  frame carrying a valid IE is rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo review addressed in 8ea6edc:

  1. Over-the-air replay: a new UE-side HoReceiver acts on a handover IE only if its id is above every one accepted and its switch instant (covered by the tag) is still ahead. A replayed old beacon is inert. Tested with the same, a superseded and a spent IE. The header documents that find_ho_ie picks the newest IE within one frame and that cross-beacon replay is rejected by HoReceiver.
  2. Non-beacon frames: find_ho_ie scans beacons only (frame control 0x80). A test puts a valid IE in a data frame and checks it is rejected.

@josephnef

Copy link
Copy Markdown
Collaborator Author

Both Qodo findings resolved. The remaining item, 'Embed handover in the grant-map IE', is Qodo's alternative-approach note, not a finding. A separate tagged IE keeps the grant map's format and its CRC-only integrity unchanged. Merging under skip-qodo-gate.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 5, 2026
@josephnef

Copy link
Copy Markdown
Collaborator Author

This PR is merged: the squash commit 2460de9 is on master, and its tree is identical to this PR's head (8ea6edc), which passed CI. GitHub returned a GraphQL error during the merge call and left the PR record open. Closing it to match. Reopen if GitHub reconciles it differently.

@josephnef josephnef closed this Nov 5, 2026
josephnef added a commit that referenced this pull request Nov 5, 2026
…itional fallback (#530)

Part of M6 (#260), epic #254. Step 2b: handover execution on top of #528
(observe-only A3) and #529 (command and IE). Full write-up in
`docs/cell-m6.md`.

- **CU (`RAN_HO_EXECUTE=1`):**
- an A3 candidate becomes a `HoCommand`: the target's free id and
position, the switch instant and the TSF delta;
- **two-phase:** sent to the target first, to the source on the target's
ack, and a cancel goes to the target if the source refuses;
  - `ran.ho_done` when the target reports the UE.
- **Target DU:** pre-admits the UE under the commanded id and publishes
its grant for the switch instant.
- **Source DU:** airs the tagged IE until the switch, then releases the
UE only if it stopped uplinking there; otherwise it keeps it.
- **UE:** at the instant it shifts its clock (`TsfSync::ShiftRemote`,
new, selftested), takes its target identity and runs the target's
buffered map. **Conditional:** with no target grant within 10
superframes it returns to the source.
- **Fix:** a DU with no UE now paces its main loop. Its superframe
count, and so its reported network superframe, used to run away.

Rig (two PTP cells; the crossing is forced with `RAN_A3_OFFSET_DB=-2`,
as the UEs are stationary):

| | normal | target DU killed right after staging |
|---|---|---|
| switch | source sf 143 → target sf 137 (= the target's effective sf) |
source sf 172 |
| target hears the UE | in slot from the first superframe after:
1115/1117 | — |
| outcome | committed; source released at +21; `ran.ho_done` +5 | UE
fell back at +10; source kept it, 1092/1092 in slot |

ctest 92/92. Not yet: DAPS (duplicated uplink plus `SeqDedup`), an
emulated moving crossing, and different-channel handover.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef josephnef mentioned this pull request Nov 5, 2026
3 of 5 tasks
github-actions Bot pushed a commit to OpenHD/devourer that referenced this pull request Nov 6, 2026
…PC#529)

Part of M6 (OpenIPC#260), epic OpenIPC#254. Step 2a, headless: the pieces that move a
UE's schedule from cell A to cell B. The DU/UE execution and the rig
test come next.

- **`src/cell/HoCommand.h`:**
- **`HoCommand`** (CU → both DUs): the UE, its id and position at the
target, the network-wide switch superframe, and the TSF delta.
- **`ho_tsf_delta_us`:** for DUs on the PTP fabric, `tsf_k(t) = t −
phase_k − net_off_k·P`, so the target's clock is the source's plus
`(phase_s − phase_t) + (net_off_s − net_off_t)·P`. A UE locked to A
therefore knows B's clock exactly, with no scan and no re-acquisition.
- **`HoIe`** (A's beacon → the UE): carries a CRC and the cell key's
HMAC tag, so a forged handover is inert. `find_ho_ie` returns the newest
command addressed to that UE.
- **F1 v4:** `Type::Handover` with a freshness `seq`; `Report.net_off`,
filled by the DU; and an `Ack.kind` so handover and schedule acks are
distinct.
- **`cell_m6` ctest:**
  - the TSF delta on a simulated two-DU timeline;
  - the handover message round trip, tags and every truncation;
- the IE inside an MPDU: newest id wins, a wrong key is inert, a torn IE
is dropped, unkeyed vs keyed.
- **Checks:** ctest 92/92, and the M5 rig run passes on F1 v4.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant