diff --git a/SPEC.md b/SPEC.md index b027caa..cb0d353 100644 --- a/SPEC.md +++ b/SPEC.md @@ -316,14 +316,36 @@ A clean-room client that targets LXMF interop only can ignore GROUP destinations Every Reticulum packet starts with a 1-byte flag field: ``` -bit 7-6 : header_type (0 = HEADER_1, 1 = HEADER_2) +bit 7 : ifac_flag (0 = open / no IFAC, 1 = IFAC field present) +bit 6 : header_type (0 = HEADER_1, 1 = HEADER_2) bit 5 : context_flag (1 = announce includes a ratchet pubkey) -bit 4 : transport_type (0 = BROADCAST, 1 = TRANSPORT) -bit 3-2 : destination_type (0=SINGLE, 1=GROUP, 2=PLAIN, 3=LINK) -bit 1-0 : packet_type (0=DATA, 1=ANNOUNCE, 2=LINKREQUEST, 3=PROOF) +bit 4 : transport_type (0 = BROADCAST, 1 = TRANSPORT) — the official manual calls this "propagation_type" +bit 3-2 : destination_type (0 = SINGLE, 1 = GROUP, 2 = PLAIN, 3 = LINK) +bit 1-0 : packet_type (0 = DATA, 1 = ANNOUNCE, 2 = LINKREQUEST, 3 = PROOF) ``` -`header_type` is a **2-bit field**. Only values `0` (HEADER_1) and `1` (HEADER_2) are defined; values `2` and `3` are RESERVED and never emitted on the wire by upstream. Bit 7 therefore reads `0` on every packet RNS 1.x produces. Implementations MUST NOT treat bit 7 as a separate flag (e.g. an IFAC indicator) — both bits are extracted as one field by upstream's parser (`(self.flags & 0b01000000) >> 6` at `RNS/Packet.py:246`), and a non-zero high bit will be interpreted as `header_type ∈ {2, 3}` and rejected. The IFAC (Interface Authentication Code) is a **separate trailing field** appended after the packet body, never sharing space with the flag byte (`IFAC_MIN_SIZE = 1` byte at `RNS/Reticulum.py:151-154`). +Each subfield is **1 bit** (or 2 for `destination_type` / `packet_type`). Upstream's parser extracts them with these masks (`RNS/Packet.py:246-250` in RNS 1.2.0): + +```python +self.header_type = (self.flags & 0b01000000) >> 6 # bit 6 only +self.context_flag = (self.flags & 0b00100000) >> 5 +self.transport_type = (self.flags & 0b00010000) >> 4 +self.destination_type = (self.flags & 0b00001100) >> 2 +self.packet_type = (self.flags & 0b00000011) +``` + +Bit 7 (`ifac_flag`) is set by `Transport.transmit` immediately before transmission when the egress interface has an IFAC identity attached (`RNS/Transport.py:993-1024`). The setter is unambiguous: + +```python +# Set IFAC flag +new_header = bytes([raw[0] | 0x80, raw[1]]) # 0x80 = bit 7 +# Assemble new payload with IFAC +new_raw = new_header + ifac + raw[2:] # IFAC field is inserted between header and addresses +``` + +When `ifac_flag = 1`, an `ifac_size`-byte IFAC field appears immediately after byte 2 of the header — i.e. between the `hops` byte and the start of `ADDRESSES`. `ifac_size` is interface-configured and ranges from `IFAC_MIN_SIZE = 1` byte (`RNS/Reticulum.py:151-154`) up to 64 bytes (full Ed25519 signature). The receiving side strips the IFAC after verification and rebuilds the un-IFACed packet for upstream processing (`RNS/Transport.py:1342-1369`). + +> ⚠️ **Spec correction.** Earlier revisions of this section (through commit [`8c4d550`](https://github.com/thatSFguy/reticulum-specifications/commit/8c4d550)) treated `header_type` as a 2-bit field occupying bits 7-6, with bit 7 reserved. That was wrong: bit 7 has always been the IFAC flag (`Transport.transmit` line 1003), and `header_type` is 1 bit at position 6. The official manual §4.6.3 documents the correct layout. Implementations that followed the prior wording will mis-parse IFAC-protected packets as `header_type = 2 or 3` and reject them. Surfaced by the reporter on [issue #4](https://github.com/thatSFguy/reticulum-specifications/issues/4) item #1. ### 2.2 Two header forms diff --git a/tools/verify_packet_header.py b/tools/verify_packet_header.py index e9d3a4c..e54d574 100644 --- a/tools/verify_packet_header.py +++ b/tools/verify_packet_header.py @@ -2,9 +2,12 @@ Verifier for SPEC.md §2.1, §2.2, §2.3. Verifies: - - §2.1: flag-byte layout (header_type, context_flag, transport_type, - destination_type, packet_type) — by constructing packets with each - combination and reading the resulting flag byte. + - §2.1: flag-byte layout (ifac_flag at bit 7, header_type at bit 6, + context_flag at bit 5, transport_type at bit 4, destination_type + at bits 3-2, packet_type at bits 1-0) — by constructing packets + with each combination and reading the resulting flag byte, and + by asserting that upstream's parse mask `0b01000000 >> 6` + treats bit 7 as separate from header_type. - §2.2: HEADER_1 layout flags(1) hops(1) dest_hash(16) context(1) data and HEADER_2 layout flags(1) hops(1) transport_id(16) dest_hash(16) context(1) data. @@ -48,10 +51,11 @@ def init_minimal_rns(): def verify_flag_byte_layout(): - # §2.1: bit 7-6 header_type, bit 5 context_flag, bit 4 transport_type, - # bit 3-2 dest_type, bit 1-0 packet_type. + # §2.1: bit 7 ifac_flag, bit 6 header_type, bit 5 context_flag, + # bit 4 transport_type, bit 3-2 dest_type, bit 1-0 packet_type. # Build a packet by hand and check the flag byte by replicating - # RNS.Packet.pack's header_type field semantics. + # RNS.Packet.pack's header_type field semantics (header_type << 6, + # i.e. 1-bit field at position 6 — NOT bits 7-6). cases = [ # (header_type, context_flag, transport_type, dest_type, packet_type, expected_flag) (0, 0, 0, 0, 0, 0b00000000), @@ -70,6 +74,41 @@ def verify_flag_byte_layout(): print("PASS S2.1 flag-byte layout") +def verify_ifac_bit_position(): + # §2.1: bit 7 (mask 0x80) is the IFAC flag, set by Transport.transmit + # at line 1003: `new_header = bytes([raw[0] | 0x80, raw[1]])`. It is + # NOT part of header_type. + # Lock in two invariants: + # 1. Setting bit 7 must NOT change the parsed header_type — upstream's + # parser at RNS/Packet.py:246 isolates bit 6 only via mask 0b01000000. + # 2. The constant 0x80 == bit 7, distinct from the header_type mask. + parse_mask = 0b01000000 + if parse_mask != 1 << 6: + fail(f"S2.1 parse_mask: header_type mask must be bit 6 (0x40), got 0x{parse_mask:02x}") + if (parse_mask & 0x80) != 0: + fail("S2.1 parse_mask: header_type mask must NOT cover bit 7") + + for ifac_set in (0, 1): + for ht_value in (0, 1): + flag = (ifac_set << 7) | (ht_value << 6) + parsed_ht = (flag & parse_mask) >> 6 + if parsed_ht != ht_value: + fail(f"S2.1 ifac_bit: flag=0x{flag:02x} (ifac={ifac_set} ht={ht_value}) " + f"parsed header_type={parsed_ht}, expected {ht_value} — " + f"bit 7 leaking into header_type would make this fail") + + # Also confirm the upstream IFAC setter constant matches our spec. + # RNS/Transport.py:1003: `bytes([raw[0] | 0x80, raw[1]])` — we lock in + # the value 0x80 (bit 7) as the IFAC flag mask. + IFAC_FLAG_MASK = 0x80 + if IFAC_FLAG_MASK != 1 << 7: + fail(f"S2.1 IFAC mask: expected 0x80 (bit 7), got 0x{IFAC_FLAG_MASK:02x}") + if IFAC_FLAG_MASK & parse_mask: + fail("S2.1 IFAC mask overlaps header_type parse mask — would re-introduce the prior-spec bug") + + print("PASS S2.1 IFAC bit position (bit 7, distinct from header_type)") + + def verify_header_two_form(): # §2.2: HEADER_2 wire form is flags(1) hops(1) transport_id(16) dest_hash(16) context(1) data. # The simplest verification is to round-trip via RNS.Packet's unpack: build @@ -182,6 +221,7 @@ def main(): rns_instance = init_minimal_rns() try: verify_flag_byte_layout() + verify_ifac_bit_position() verify_header_two_form() verify_header_conversion(rns_instance) finally: