Skip to content

Routing Redesign Phase 2 Wire-Spec Plan

Status: documentation-only preparation. This note intentionally changes no packet layout, codec, golden bytes, firmware behavior, daemon behavior, or tests. It is the approval checklist for the later Phase 2.1/2.2/2.3 implementation pass.

Phase 2 is a deliberate LPP wire event. Do not implement it until the PR records:

  • /lrc-wire-change completion.
  • Human RF sign-off for ERB/RACK/TX-contention behavior per AGENTS.md rule 5.
  • The docs/PROTOCOL.md LPP v2 migration note described below before any golden bytes move.

Implement Phase 2 as an LPP v2 event (VT version bits 01), not as a same-version v1.1 break. Reusing bit 3 with a different optional-block layout is exactly the compatibility hazard the wire-change checklist calls out: old v1 nodes parse the ERB head as path_len, so old and new nodes can disagree about packet boundaries on the same channel.

Approval target:

  • docs/PROTOCOL.md states that v2 replaces v1.1 FLAGS.PATH with FLAGS.ERB, and that mixed v1/v2 edge relaying is not guaranteed for ERB frames unless a later compatibility shim is deliberately designed.
  • core/src/packet.cpp::encode() emits VT=01 for the new layout, and decode() accepts that version only for the v2 ERB/RACK layout.
  • Existing path-block behavior is either removed with the v2 migration or kept explicitly fenced as legacy v1 decode/relay behavior; it must not be silently reinterpreted as ERB in v1.1.

Open Decision: Observed Successful Delivery

Section titled “Open Decision: Observed Successful Delivery”

Recommendation for Phase 2.3: define “observed successful delivery” as a router-local overhear event, not a new relay-sourced confirmation frame for the first implementation. A relay is admitted to RELAY_VIA only when the router overhears that relay advance one of the router’s own downlink frames toward the client, with attribution derived from the router’s receiver state and the ERB state of that downlink.

Tradeoff:

  • Pro: no new trust-bearing wire claim; works for keyless foreign relays; avoids adding another R6 freshness/Tag8 surface; keeps ERB.TRAIL[] non-trust data.
  • Con: slower admission under asymmetric or hidden-node geometry where the client hears the relay but the router cannot overhear the relay. Those cases stay in BROADCAST/UNKNOWN until a router-observed event occurs.

Defer relay-sourced delivery confirmations. If they are later needed for the hidden-node case, they need a separate wire-change approval: exact packet type or flag, freshness binding, authenticated source rule for registered relays, and the keyless fallback behavior. A keyless relay confirmation must not become a trust input.

Future approval target: replace FLAGS.PATH with FLAGS.ERB and replace the path block with the ERB slot described in ASYNC_ROUTING_DESIGN.md section 6.2:

byte 0: ERB_VER[7..6] | mode[5..4] | rprog[3..0]
byte 1: HINT
byte 2: RACK_RTR1
byte 3: reserved[7..4] | TRAIL_LEN[3..0]
byte 4..: TRAIL[TRAIL_LEN]

Files and functions that must be touched once approved:

  • docs/PROTOCOL.md: common FLAGS table, Path block section replacement, Tag8/canonical-image text, ACK/HELLO path language, MTU/versioning notes.
  • docs/IDENTITY.md: registration path text in section 2 and canonical-image language in section 7.
  • docs/ROUTING.md and docs/TELEMETRY.md: replace path-pinning/store-forward text and stable counter names with ERB/RD terms.
  • core/include/lrc/packet.h: flags::Path, flags::kMutableMask, Packet::path_len, Packet::path[], and any ERB replacement struct/accessors.
  • core/src/packet.cpp: Offsets, layout(), encode(), decode(), canonical_image(), and comments describing mutable-region exclusions.
  • core/src/reg.cpp and core/include/lrc/reg.h: RegClient::make_hello(), RegClient::handle_welcome(), Registrar::handle_hello(), and the router_path_* state/accessors.
  • core/src/node.cpp: append_path_discovery_hop(), reset_path_block_to_discovery(), Node::remote_privmsg(), Node::send_dccctl(), Node::send_dccdata(), Node::handle_keyreq(), Node::service_key_lookup(), Node::send_ack(), Node::handle_ack(), Node::retry_pending_acks(), Node::reroute_pending_path_ack(), Node::handle_frame(), Node::apply_path_pin(), Node::note_reverse_path_pin(), and Node::clear_path_pin().
  • core/include/lrc/node.h: PendingAck::path_pinned, PathPin, path_pins_, and the path helper declarations.
  • firmware/src/main.cpp: handle_radio_frame() and any firmware-owned HELLO, WELCOME, beacon, or relay behavior that reads or emits the old path slot.

Tests that must move or be added:

  • tests/test_packet.cpp: packet_dm_with_path, packet_router_restamp_keeps_tag_valid, packet_golden_bytes, packet_decode_rejects_garbage, packet_decode_fuzz_smoke, plus new hand-built ERB golden bytes and canonical-image mutation coverage.
  • tests/test_reg.cpp: reg_welcome_returns_reverse_path_and_client_pins_router_path and reg_hello_retry_budget_clears_pinned_router_path.
  • tests/test_node.cpp: node_path_block_relays_matching_next_hop, node_path_relay_store_forwards_when_egress_returns, node_path_discovery_appends_relay_uid_and_preserves_tag, node_path_ack_pins_discovered_route_for_next_dm, node_path_ack_timeout_clears_pin_and_rediscovers_next_dm, node_standby_failover_reroutes_pinned_dm_retry_to_discovery, node_path_block_does_not_flood_unlisted_relay, node_path_block_blind_relays_unknown_version_matching_next_hop, node_path_discovery_blind_relays_unknown_version, node_path_block_unknown_version_does_not_flood_unlisted_relay, and the TX-off relay-store tests around lines 8130-8172.
  • tests/test_chaos.cpp: add ERB/RD multi-hop cases only after the host decision layer exists; these belong naturally with Phase 3.1.

Golden-byte/versioning implications:

  • Reusing bit 3 with a different optional-block layout is not wire-compatible with current v1 nodes. Old nodes will parse the ERB head as path_len, which can make old and new nodes disagree about packet boundaries. This is a /lrc-wire-change red flag.
  • The approval PR must bump the VT version and document that mixed v1/v2 edge relaying is not guaranteed for ERB frames unless explicit legacy interop code is added.
  • tests/test_packet.cpp::packet_golden_bytes and every new ERB golden frame must be rebuilt by hand, not copied from encoder output.

Future approval target: reserve an exact group-C RACK packet type, preferably the next unused group-C value 0x2B, with PRIO, TTL=1, ERB mode=RACK, and no relay past one hop. LANEGRANT remains unchanged. Resolve the “empty payload” draft conflict by carrying a freshness-bound target in the payload:

u64 target_dedup_key_le
u32 router_boot_id_or_session_counter_le

The authenticated trust input is the tagged packet type, SRC, target/freshness payload, and the normal dedup gate. ERB mode and RACK_RTR1 stay mutable hints and must not be the sole authority for canceling a relay.

Files and functions that must be touched once approved:

  • docs/PROTOCOL.md: Packet type table, RACK frame layout, RACK authentication, freshness, replay, and “never relayed past one hop” rules.
  • core/include/lrc/packet.h: PacketType::Rack and any ERB mode constants.
  • core/src/packet.cpp: known_type() and any length-exact RACK/ERB decode constraints.
  • core/src/node.cpp: add RACK emission/handling on the normal handle_frame() path after decode(), tag/signature checks, and dedup; do not add a fast path that bypasses dedup_.seen_or_insert(). Use Node::emit_lane_grant() and Node::handle_lane_grant() as the local Tag8/src-check pattern.
  • core/include/lrc/node.h: declare any RACK handler/emitter state.
  • firmware/src/main.cpp: add firmware RACK RX/TX only after RF sign-off; it must respect CAD, duty-cycle, and local TX enable policy.
  • core/include/lrc/telemetry.h and docs/TELEMETRY.md: add stable counters for RACK TX/RX, bad authentication, cancel, and shorten-only fallback if the behavior is observable.

Tests that must move or be added:

  • tests/test_packet.cpp: new packet_rack_golden_bytes, strict freshness-payload decode tests, and bad-length rejection.
  • tests/test_node.cpp: registered relay verifies SK-Tag8 and src, duplicate RACK is deduped, wrong source/bad tag cannot cancel, and keyless relay can shorten but never cancel.
  • tests/test_chaos.cpp: suppression-abuse scenarios belong with Phase 3.1 once Phase 1 host decision logic exists.

Red flag resolved for the next implementation pass: NEXT_PASSES.md originally said RACK has an empty payload, while Rule C requires a freshness binding (boot_id or a monotone per-session counter) and Rule A needs the signal to name the logical frame being suppressed. Use the target/freshness payload above unless a later wire-change review deliberately replaces it with an equally explicit covered binding.

Future approval target: with the recommended router-overhear choice, this is a normative spec and state-machine change, not an additional packet layout beyond ERB/RACK. docs/PROTOCOL.md must say that no relay is admitted to RD RELAY_VIA from TRAIL[]; the admission event is the router overhearing a relay advance one of the router’s own downlink frames.

Files and functions that must be touched once approved:

  • docs/PROTOCOL.md: define the observed-delivery event and state that it is router-local authority, not a TRAIL byte or relay claim.
  • docs/ROUTING.md and docs/TELEMETRY.md: document RD admission, candidate aging, and counters.
  • core/include/lrc/node.h: add bounded RD and return-candidate state once the Phase 1 host decision layer has landed.
  • core/src/node.cpp: integrate the RD gate in Node::handle_frame(), ACK/DM delivery handling (Node::send_ack(), Node::handle_ack()), relay emission, and candidate aging. TRAIL[] may propose/order only already gated candidates.
  • core/include/lrc/telemetry.h: add counters for observed delivery, candidate admission, candidate expiry, and rejected TRAIL-only attempts if surfaced through stats.

Tests that must move or be added:

  • tests/test_node.cpp: first contact is BROADCAST or UNKNOWN, never RELAY_VIA; router-overheard relay forwarding admits a candidate; TRAIL-only candidates age out; poisoned TRAIL cannot pin return; RD decays to UNKNOWN.
  • tests/test_chaos.cpp: asymmetric-return correctness and no permanent black-hole under suppression.

If the approval decision instead chooses relay-sourced delivery confirmations, Phase 2.3 becomes another packet-layout event: add the packet/flag, freshness, Tag8/source rules, golden bytes, and keyless fallback to this plan before code.

Stop and ask before coding if any of these remain unresolved:

  • Old and new nodes disagree about packet boundaries after bit-3 reuse.
  • RACK freshness depends on an unstated receiver-side fact.
  • Any ERB field starts feeding a trust decision.
  • Any Tag8/canonical-image coverage changes beyond masking the ERB slot.
  • Any UID derivation, dedup key, or SEQ3 canonical stamping changes.
  • Any firmware TX/contention behavior lands without human RF sign-off.

Minimum approval-time verification:

git diff --check
cmake --build build -j
./build/lrc_tests
python3 tests/smoke_lrcd.py
(cd firmware && pio run)