2/RF

Network Protocol (RF Network Layer)

Core Protocol Specification Part 1 - The Network Protocol (RF Network Layer)

status
raw

Metadata

Detail Description
Origin

Derived from ZephCore firmware implementation, cross-referenced with MeshCore reference firmware and documentation.

Scope

This document defines The Network Protocol — the RF network protocol used by nodes to communicate over a LoRa mesh. It specifies packet format, routing, payload structures, and cryptography.

Companion document

The host-side protocol used by applications to control and query a node over a local transport (serial/USB, Bluetooth Low Energy, or TCP) is defined separately in 3/HOST. This document and the Companion Protocol document share the conventions defined in §1.

Preface

This document is written to be implementation-agnostic. Field names, constants, and structures are described in neutral terms, not tied to any specific language or firmware. Where pseudo-code is provided, it is illustrative only.

1. Conventions and Terminology

1.1. Notation

1.2. Byte Order

1.3. Strings

1.4. Terminology

1.5. Protocol Layering

A compliant implementation consists of three layers:

┌─────────────────────────────────────────────────────┐
│  Application (contacts, channels, messaging, UI)    │
├─────────────────────────────────────────────────────┤
│  The Companion Protocol (host ↔ node)               │  ← Part 2 (companion document)
├─────────────────────────────────────────────────────┤
│  The Protocol (RF network)                          │  ← this document (Part 1)
├─────────────────────────────────────────────────────┤
│  Physical Layer (LoRa radio)                        │
└─────────────────────────────────────────────────────┘

The Protocol and the Companion Protocol are independent. A node that speaks only The Protocol (e.g., a repeater with no host interface) is compliant. A host library that speaks only the Companion Protocol (without touching the radio) is compliant. The two are bridged by the node firmware.


2. The Protocol (RF Network Layer)

This part specifies the packet format, routing, payload structures, and cryptography used by nodes to communicate over the LoRa RF mesh.

2.1. Packet Structure

Every packet transmitted over the air has the following layout:

┌──────────┬─────────────────────┬─────────────┬──────┬──────────┐
│  header  │  transport_codes    │ path_length │ path │ payload  │
│  1 byte  │ 4 bytes (optional)  │   1 byte    │  V   │    V     │
└──────────┴─────────────────────┴─────────────┴──────┴──────────┘
FieldSizeNotes
header1 byteEncodes route type, payload type, and payload version.
transport_codes4 bytes (optional)Present only when route type is a “transport” variant (§2.4).
path_length1 byteEncodes hop count and per-hash size (§2.5).
pathvariableZero or more node hashes (§2.5). May be empty.
payloadvariablePayload-type-specific data (§2.6).

2.1.1. Size Limits

ConstantValueDescription
MAX_PACKET_PAYLOAD184 bytesMaximum payload length.
MAX_PATH_SIZE64 bytesMaximum total bytes in the path field.
MAX_TRANS_UNIT255 bytesMaximum total on-wire packet size (physical-layer FIFO size).

Receivers MUST drop any packet whose payload length exceeds MAX_PACKET_PAYLOAD or whose total length exceeds MAX_TRANS_UNIT. Receivers MUST drop packets whose path byte-length exceeds MAX_PATH_SIZE.


2.2. Header Encoding

The header byte packs three fields:

  bit 7   6   5   4   3   2   1   0
      │   │   │           │   │   │
      └─┬─┘   └─────┬─────┘   └─┬─┘
        │           │           │
     Payload     Payload      Route
     Version     Type         Type
     (2 bits)    (4 bits)     (2 bits)
BitsMaskFieldDescription
0–10x03Route TypeSee §2.3.
2–50x3CPayload TypeSee §2.6.
6–70xC0Payload VersionProtocol versioning of the payload wrapper.

2.2.1. Payload Version

The current specification defines only version 1. Versions 2–4 are reserved.

ValueVersionMeaning
0b0011-byte source/destination node hashes, 2-byte MAC.
0b012Reserved for future use (e.g., 2-byte hashes, 4-byte MAC).
0b103Reserved.
0b114Reserved.

Nodes implementing this specification MUST emit version 1 only. Nodes MUST ignore packets whose payload version is unknown.

2.2.2. Sentinel Header Value

The value 0xFF for the entire header byte is reserved as a local in-memory “do not retransmit” marker. It MUST NOT appear on the wire. Receivers that see 0xFF on the wire MUST drop the packet.


2.3. Route Types

The low 2 bits of header specify how the packet is routed.

ValueNameDescription
0x00ROUTE_TYPE_TRANSPORT_FLOODFlood routing, filtered by a transport-code scope.
0x01ROUTE_TYPE_FLOODFlood routing (no scope).
0x02ROUTE_TYPE_DIRECTDirect routing along an explicit path.
0x03ROUTE_TYPE_TRANSPORT_DIRECTDirect routing, filtered by a transport-code scope.

2.4. Transport Codes

When the route type is ROUTE_TYPE_TRANSPORT_FLOOD or ROUTE_TYPE_TRANSPORT_DIRECT, the header is immediately followed by four bytes:

OffsetSizeFieldDescription
02transport_code_1Primary scope code (unsigned 16-bit, little-endian).
22transport_code_2Secondary scope code (reserved; unused in v1).

2.4.1. Transport Code Derivation

A transport code is computed from a 16-byte transport key and the packet’s payload:

message   = payload_type_byte || payload
hmac      = HMAC-SHA256(key = transport_key, data = message)
code      = (hmac[0]) | (hmac[1] << 8)            # little-endian 16-bit
if code == 0x0000:  code = 0x0001                 # reserved
if code == 0xFFFF:  code = 0xFFFE                 # reserved

The codes 0x0000 and 0xFFFF are reserved and MUST NOT appear on the wire; implementations MUST coerce them to 0x0001 and 0xFFFE respectively.

A node that does not share a given transport key cannot verify a matching transport code and therefore MUST NOT forward transport-scoped packets whose code does not match any of its loaded transport keys.

transport_code_2 is reserved; senders MUST set it to 0x0000 and receivers MUST accept any value without treating it as a match criterion.


2.5. Path Encoding

The path_length byte encodes two fields:

  bit 7   6   5   4   3   2   1   0
      │   │   │   │   │   │   │   │
      └─┬─┘   └───────┬───────────┘
        │             │
  Hash Size       Hop Count
  Code (2 bits)   (6 bits, 0–63)
BitsFieldMeaning
0–5Hop CountNumber of path hashes present (0 to 63).
6–7Hash Size CodePer-hash byte size, minus 1.
Hash Size CodePer-hash SizeNotes
0b001 byteLegacy; default.
0b012 bytesSupported.
0b103 bytesSupported.
0b11(reserved)Invalid — drop packet.

The on-wire path field is exactly hop_count × hash_size bytes. Receivers MUST compute the byte length from these two values; the path_length byte is NOT a raw byte count.

2.5.1. Path Examples

path_lengthHash SizeHop Countpath byte length
0x00100
0x05155
0x452510
0x8A31030

2.5.2. Validity

A path_length byte is valid if and only if:

Receivers MUST drop packets with invalid path_length.

2.5.3. Node Hash Derivation

A node hash of size N is the first N bytes of the node’s Ed25519 public key. All nodes in a given path-hash-mode deployment MUST use the same hash size for a packet’s path field.


2.6. Payload Types

Bits 2–5 of header specify the payload type. The payload structure for each type is defined in sections 2.8–2.16.

ValueNameDescription
0x00PAYLOAD_TYPE_REQEncrypted request to a known peer.
0x01PAYLOAD_TYPE_RESPONSEEncrypted response to a REQ or ANON_REQ.
0x02PAYLOAD_TYPE_TXT_MSGEncrypted text message.
0x03PAYLOAD_TYPE_ACKAcknowledgement.
0x04PAYLOAD_TYPE_ADVERTSigned node advertisement.
0x05PAYLOAD_TYPE_GRP_TXTGroup (channel) text message.
0x06PAYLOAD_TYPE_GRP_DATAGroup (channel) datagram.
0x07PAYLOAD_TYPE_ANON_REQAnonymous (unsolicited, signed) request.
0x08PAYLOAD_TYPE_PATHPath return (route discovery response).
0x09PAYLOAD_TYPE_TRACETrace route with per-hop SNR.
0x0APAYLOAD_TYPE_MULTIPARTMulti-packet composite (currently, multi-ack).
0x0BPAYLOAD_TYPE_CONTROLUnencrypted control data.
0x0C(reserved)
0x0D(reserved)
0x0E(reserved)
0x0FPAYLOAD_TYPE_RAW_CUSTOMRaw bytes with caller-defined encryption.

2.7. Cryptography

The following primitives are used in The Protocol:

PurposeAlgorithm
Node identity, signingEd25519
Key agreementX25519 (performed on the Ed25519 keypair’s Montgomery-form equivalent, as in ed25519_key_exchange)
Symmetric encryptionAES-128 in ECB mode with zero padding
Message authentication codeHMAC-SHA256, truncated to the first 2 bytes
HashingSHA-256

2.7.1. Identity

Each node has an Ed25519 keypair:

The node hash used in paths and routing is a 1, 2, or 3-byte prefix of the public key (§2.5.3).

2.7.2. Shared Secret

The shared secret between two nodes is derived by X25519 key exchange on their Ed25519 keypairs:

shared_secret = X25519(my_private_key, their_public_key)    # 32 bytes

Both sides produce the same 32-byte secret.

2.7.3. Encryption

AES-128-ECB with zero padding. The first 16 bytes of the 32-byte shared secret are used as the AES key.

Plaintext is padded with zero bytes up to a multiple of 16. Ciphertext length equals padded plaintext length.

aes_key   = shared_secret[0..16]                           # first 16 bytes
padded    = plaintext || zero_pad_to_multiple_of(16)
ciphertext = AES-128-ECB-encrypt(aes_key, padded)

Decryption reverses the process. The receiver cannot distinguish trailing zero bytes added by padding from trailing zero bytes in the original plaintext; application payloads either carry an internal length field or tolerate trailing zeros.

2.7.4. Message Authentication Code (MAC)

The MAC is the first 2 bytes of HMAC-SHA256 computed over the ciphertext, using the full 32-byte shared secret as the HMAC key.

mac = HMAC-SHA256(key = shared_secret, data = ciphertext)[0..2]

Wire format for an encrypted region is mac || ciphertext (MAC first, then ciphertext). Receivers MUST verify the MAC before decrypting; packets with mismatched MACs MUST be silently dropped.

Note on the HMAC key: The HMAC key uses the full 32-byte shared secret, even though the AES key uses only the first 16 bytes. Implementations that truncate the HMAC key to 16 bytes will not interoperate.

2.7.5. Channel Key Derivation

A group channel has a symmetric secret. The secret is either 16 or 32 bytes:

The 1-byte channel hash used on the wire is:

channel_hash = SHA-256(channel_secret)[0]    # first byte

Where channel_secret is the raw 16-byte (or 32-byte) secret bytes.

2.7.6. Well-Known Channel Keys


2.8. Advertisement Payload

Payload type PAYLOAD_TYPE_ADVERT (0x04). Broadcast by every node to announce its presence. The payload is not encrypted but is signed by the advertising node.

┌───────────────┬───────────┬───────────┬────────────┐
│  public_key   │ timestamp │ signature │  app_data  │
│    32 bytes   │  4 bytes  │ 64 bytes  │ 0–32 bytes │
└───────────────┴───────────┴───────────┴────────────┘

| Field | Size | Description | | ------------ | ------------- | --------------------------------------------------------- | --- | --------- | --- | ---------- | | public_key | 32 | The advertising node’s Ed25519 public key. | | timestamp | 4 | Unix timestamp (seconds, unsigned 32-bit, little-endian). | | signature | 64 | Ed25519 signature covering public_key | | timestamp | | app_data. | | app_data | 0 to 32 bytes | Optional application-level metadata. See below. |

The maximum app_data length is MAX_ADVERT_DATA_SIZE = 32 bytes. Receivers MUST clip app_data to 32 bytes if the payload length suggests otherwise.

2.8.1. Signature Verification

A receiver verifies the signature by computing:

message = public_key || timestamp || app_data
valid   = Ed25519-verify(signature, message, public_key)

Receivers MUST drop adverts with invalid signatures.

2.8.2. app_data Structure

When present, app_data begins with a flags byte that indicates which optional fields follow:

┌───────┬───────────┬───────────┬──────────┬──────────┬─────────────────┐
│ flags │ latitude  │ longitude │ feature1 │ feature2 │      name       │
│ 1 byte│ 4 (opt)   │ 4 (opt)   │ 2 (opt)  │ 2 (opt)  │ 0–N bytes (opt) │
└───────┴───────────┴───────────┴──────────┴──────────┴─────────────────┘

Fields appear in the listed order; each is present if and only if the corresponding flag bit is set. The name field, if present, fills the remainder of app_data.

2.8.3. flags Byte

The flags byte is split into a 4-bit node type (low nibble) and 4 presence flag bits (high nibble).

BitsMaskFieldDescription
0–30x0FNode TypeSee table below.
40x10Has Locationlatitude and longitude are present.
50x20Has Feature 1feature1 is present. Reserved; currently unused.
60x40Has Feature 2feature2 is present. Reserved; currently unused.
70x80Has Namename is present.

Node Type values (low 4 bits):

ValueNameDescription
0x00ADV_TYPE_NONEUnspecified / generic node.
0x01ADV_TYPE_CHATInteractive chat node (phone companion).
0x02ADV_TYPE_REPEATERStore-and-forward repeater.
0x03ADV_TYPE_ROOMRoom server (multi-user chat host).
0x04ADV_TYPE_SENSORSensor node (telemetry publisher).
0x050x0F(reserved)

Clarification: Earlier documentation listed the node-type values among the presence flag bits, which is incorrect. They occupy the low 4 bits of the flags byte. The high 4 bits are the presence flags.

2.8.4. Location Encoding

If the Has Location flag is set:

OffsetSizeFieldDescription
04latitudeSigned 32-bit integer, little-endian. degrees × 1,000,000.
44longitudeSigned 32-bit integer, little-endian. degrees × 1,000,000.

Example: latitude = 49123456 encodes 49.123456°.

2.8.5. Name

If the Has Name flag is set, all remaining bytes of app_data are the UTF-8 node name. There is no null terminator on the wire.


2.9. Direct-Encrypted Payloads (Request / Response / Text / Path)

Payload types PAYLOAD_TYPE_REQ (0x00), PAYLOAD_TYPE_RESPONSE (0x01), PAYLOAD_TYPE_TXT_MSG (0x02), and PAYLOAD_TYPE_PATH (0x08) share a common wrapper:

┌──────────────────┬─────────────┬──────┬────────────┐
│ destination_hash │ source_hash │ mac  │ ciphertext │
│     1 byte       │   1 byte    │  2   │   variable │
└──────────────────┴─────────────┴──────┴────────────┘
FieldSizeDescription
destination_hash1First byte of the destination node’s public key.
source_hash1First byte of the source node’s public key.
mac2MAC of ciphertext (§2.7.4).
ciphertextVAES-encrypted plaintext (§2.7.3). The plaintext structure is payload-type-specific.

The shared secret used for encryption and MAC is derived between the source and destination via X25519 (§2.7.2).

2.9.1. Receiver Processing

When a node receives a packet with one of these payload types:

  1. If destination_hash does not match any of the node’s own hashes, treat as not-for-me (but may still forward per routing rules).
  2. For each known peer whose source_hash matches, derive the shared secret, verify the MAC, decrypt. If MAC verification succeeds, process the plaintext.
  3. If no peer matches or no MAC verifies, the packet is either not for this node or is malformed; treat as not-for-me.

2.9.2. REQ Plaintext (Request)

┌───────────┬───────────────────────┐
│ timestamp │     request_data      │
│  4 bytes  │  rest of plaintext    │
└───────────┴───────────────────────┘

Common request sub-types (first byte of request_data):

ValueNameDescription
0x01REQ_TYPE_GET_STATUSQuery node status (battery, counters).
0x02REQ_TYPE_KEEP_ALIVEMaintain a session.
0x03REQ_TYPE_GET_TELEMETRYQuery telemetry (CayenneLPP).

Other sub-types are application-defined and out of scope for this specification.

2.9.3. RESPONSE Plaintext

┌───────────────────────────────────┐
│           response_data           │
│         rest of plaintext         │
└───────────────────────────────────┘

Responses are opaque to this layer. The format is determined by the application and the corresponding request.

2.9.4. TXT_MSG Plaintext

┌───────────┬─────────────────┬─────────────────┐
│ timestamp │ txt_type_attempt│    message      │
│  4 bytes  │     1 byte      │ rest of plaintxt│
└───────────┴─────────────────┴─────────────────┘

The txt_type_attempt byte packs two fields:

BitsMaskFieldDescription
0–10x03AttemptRetry counter (0–3).
2–70xFCText TypeShifted right by 2 to produce a 6-bit value.

Defined text types (after right-shift):

ValueNameMessage Content
0x00TXT_TYPE_PLAINUTF-8 plain text message.
0x01TXT_TYPE_CLI_DATACLI command text (sent to a node’s command processor).
0x02TXT_TYPE_SIGNED_PLAINFirst 4 bytes are the sender’s public-key prefix, remainder is UTF-8 text.

2.9.5. PATH Plaintext (Returned Path)

A path-return packet communicates a discovered route back to an originator.

┌─────────────┬──────────┬────────────┬────────────────┐
│ path_length │   path   │ extra_type │  extra_payload │
│   1 byte    │ variable │   1 byte   │    variable    │
└─────────────┴──────────┴────────────┴────────────────┘
FieldSizeDescription
path_length1Same encoding as in the packet header (§2.5).
pathvariablehop_count × hash_size bytes.
extra_type1Low 4 bits: another payload type (typically ACK or RESPONSE). 0xFF means “no extra”.
extra_payloadvariableContent of the bundled extra payload, or 4 random bytes when extra_type = 0xFF.

When extra_type is 0xFF (no extra), senders MUST follow it with 4 random bytes. This prevents the encrypted block from being a known-plaintext target.


2.10. Anonymous Request Payload

Payload type PAYLOAD_TYPE_ANON_REQ (0x07). Used when the sender is not yet a known contact of the destination; the sender includes their full public key inline so the destination can derive a shared secret.

┌──────────────────┬────────────────┬──────┬────────────┐
│ destination_hash │ sender_pubkey  │ mac  │ ciphertext │
│     1 byte       │    32 bytes    │  2   │   variable │
└──────────────────┴────────────────┴──────┴────────────┘
FieldSizeDescription
destination_hash1First byte of the destination’s public key.
sender_pubkey32The sender’s Ed25519 public key.
mac2MAC of ciphertext.
ciphertextVAES-encrypted plaintext.

The destination derives the shared secret via X25519 between its own private key and the inline sender_pubkey, then MAC-verifies and decrypts.

2.10.1. Common Anonymous-Request Plaintext Formats

The plaintext format depends on the use case. Common examples:

Room-server login:

┌───────────┬────────────────┬──────────┐
│ timestamp │ sync_timestamp │ password │
│  4 bytes  │    4 bytes     │ variable │
└───────────┴────────────────┴──────────┘

Repeater / sensor login:

┌───────────┬──────────┐
│ timestamp │ password │
│  4 bytes  │ variable │
└───────────┴──────────┘

Repeater sub-request (regions / owner info / clock-and-status):

┌───────────┬──────────┬────────────────┬────────────┐
│ timestamp │ req_type │ reply_path_len │ reply_path │
│  4 bytes  │  1 byte  │     1 byte     │  variable  │
└───────────┴──────────┴────────────────┴────────────┘

Where req_type is 0x01 (regions), 0x02 (owner info), or 0x03 (clock and status). The reply_path uses the same encoding as §2.5.

Other plaintext formats are application-defined.


2.11. Group Payloads

Payload types PAYLOAD_TYPE_GRP_TXT (0x05) and PAYLOAD_TYPE_GRP_DATA (0x06). Used for channel (group) messaging with a pre-shared channel secret.

┌──────────────┬─────┬────────────┐
│ channel_hash │ mac │ ciphertext │
│   1 byte     │  2  │  variable  │
└──────────────┴─────┴────────────┘
FieldSizeDescription
channel_hash1First byte of SHA-256(channel_secret) (§2.7.5).
mac2MAC of ciphertext, keyed by the channel secret.
ciphertextVAES-encrypted plaintext, keyed by the channel secret.

The maximum plaintext length for group payloads is MAX_PACKET_PAYLOAD − 16 − 3 = 165 bytes (accounting for cipher block rounding, channel_hash, and mac).

2.11.1. GRP_TXT Plaintext

Same format as TXT_MSG plaintext (§2.9.4). The convention is that the message body is of the form sender_name: message_body so receivers can display the sender.

2.11.2. GRP_DATA Plaintext

Application-defined. The first byte typically identifies a data sub-type; the remainder is sub-type-specific data.


2.12. Acknowledgement Payload

Payload type PAYLOAD_TYPE_ACK (0x03).

┌──────────┐
│ ack_hash │
│ 4 bytes  │
└──────────┘
FieldSizeDescription
ack_hash4First 4 bytes of a SHA-256 hash computed over a message-type-dependent buffer (§2.12.1).

The field is conventionally called a “checksum” in parts of the reference firmware, but it is NOT a CRC — it is the first 4 bytes (not a truncated CRC-32) of a SHA-256 digest, used as a short collision-resistant identifier of the specific message being acknowledged.

2.12.1. ACK Hash Computation

The hash is computed over a buffer whose contents depend on the message type being acknowledged. The recipient or sender’s public key is appended to the hashed data to “salt” the hash — ensuring that different recipients of the same broadcast text produce different ACK hashes, so that senders can tell which peer acknowledged.

For TXT_MSG with txt_type == TXT_TYPE_PLAIN (0x00) or TXT_TYPE_CLI_DATA (0x01):

buffer    = timestamp (4 bytes LE)
          ∥ txt_type_attempt (1 byte)
          ∥ message_text (UTF-8, no terminator)
          ∥ sender_public_key (32 bytes)
ack_hash  = SHA-256(buffer)[0..4]        ; first 4 bytes

CLI-type messages (TXT_TYPE_CLI_DATA) MUST NOT produce an ACK on the wire despite being hashable. The sender does not track an expected hash for CLI messages; only TXT_TYPE_PLAIN triggers the full send/ack lifecycle.

For TXT_MSG with txt_type == TXT_TYPE_SIGNED_PLAIN (0x02):

buffer    = timestamp (4 bytes LE)
          ∥ txt_type_attempt (1 byte)
          ∥ sender_pubkey_prefix (4 bytes)
          ∥ message_text (UTF-8, no terminator)
          ∥ recipient_public_key (32 bytes)
ack_hash  = SHA-256(buffer)[0..4]        ; first 4 bytes

The sender’s 4-byte public-key prefix that appears at the start of a signed-plain plaintext (§2.9.4) participates in the hash; otherwise the hash input is identical in structure to the plain form.

Salting note. The public key is appended to the message bytes but is NOT transmitted as part of the hash input over the air — it is implicitly known to both sides (sender knows it from its contact record; recipient knows its own key). This turns the 4-byte ACK hash into a message-AND-recipient identifier, enabling the sender to match incoming ACKs even when the same message has been sent to multiple contacts.

2.12.2. Sender Expectations

A sender that emits a TXT_MSG requesting acknowledgement SHOULD:

  1. Compute the expected ack_hash using the public key at send time. NOTE The public key differs between TXT_TYPE_PLAIN and TXT_TYPE_SIGNED_PLAIN.
  2. Store the expected hash in a pending-ack table, keyed to the recipient contact and a send timestamp.
  3. On receipt of a PAYLOAD_TYPE_ACK whose 4-byte ack_hash matches a pending entry, mark the message as delivered and discard the entry.

The same ACK MAY be received multiple times if the acknowledging peer retransmits (e.g., via multi-ACK, §2.14); senders SHOULD tolerate this and treat only the first arrival as delivery confirmation.

2.12.3. CRC-32 Historical Note

Earlier drafts of this specification described the ack_hash field as a little-endian CRC-32. That was incorrect. The reference firmware (BaseChatMesh.cpp:221-222, 248-249) uses SHA-256(...)[0..4] exclusively. An implementation that computes CRC-32 over the same input buffer will not match any valid ACK and will fail all deduplication checks.


2.13. Trace Payload

Payload type PAYLOAD_TYPE_TRACE (0x09). Used to discover and measure a path through the mesh, collecting a per-hop SNR sample at each forwarding node.

TRACE packets use an unusual wire layout that repurposes fields from the normal packet structure. Three specific oddities distinguish TRACE from every other payload type:

  1. The header’s path_length byte is a hop-consumption counter, not an encoded path-length (§2.5). The low 6 bits start at zero and increment by one at each forwarding hop as the packet travels. The top 2 bits of the byte remain zero; the actual per-hash byte size used for the hop sequence is carried in the flags byte of the payload instead.
  2. The original hop-hash sequence is appended to the payload, after the 9-byte preamble — not placed in the header’s path field as with other direct-routed payloads. Forwarders never modify these hashes; they remain in the payload for the life of the packet.
  3. The header’s path field accumulates SNR measurements, one signed byte per consumed hop, written at position path_length immediately before path_length is incremented. By the time the packet reaches its final destination, the header’s path buffer holds the sequence of measured link qualities in traversal order.

2.13.1. Payload Layout

┌─────┬───────────┬───────┬────────────────────┐
│ tag │ auth_code │ flags │   path_hashes      │
│  4  │     4     │   1   │ variable (per hop) │
└─────┴───────────┴───────┴────────────────────┘
FieldSizeDescription
tag4Request identifier chosen by the originator (little-endian). Echoed in the trace result reported to the originator.
auth_code4Application-defined authentication code (little-endian). Opaque to this layer.
flags1Low 2 bits: path hash size code. Upper 6 bits: reserved, MUST be zero.
path_hashesVThe original ordered sequence of hop hashes the originator intends the packet to traverse. hop_count × hash_size bytes. Never modified during forwarding.

Flags byte:

BitsMaskFieldDescription
0–10x03Path Hash Sizepath_hash_size = 1 << bits (yielding 1, 2, or 4 bytes per hash).
2–70xFC(reserved)Set to 0.

Note that the TRACE flags’ path-hash-size encoding (1 << bits, values 1/2/4) differs from the encoding used in the normal packet header’s path_length byte (§2.5), which encodes hash sizes 1/2/3 as code + 1. TRACE’s encoding permits a 4-byte hash size that the normal header cannot represent.

2.13.2. Header Fields for TRACE

The packet header fields adjacent to the payload carry protocol-repurposed meaning:

Header fieldNormal meaningTRACE meaning
path_length (low 6 bits)Encoded hop countNumber of hops consumed so far (starts at 0; incremented each forwarding hop).
path_length (top 2 bits)Hash size codeMUST be zero for TRACE. The true hash size lives in the payload’s flags byte.
path fieldOrdered hop hashesAccumulated SNR measurements: one int8 (SNR_dB × 4) per consumed hop, in order.

The header’s path field is thus written at byte positions (one per consumed hop), not at hash positions, even when path_hash_size > 1. This is valid because each SNR sample is always exactly one byte.

2.13.3. SNR Encoding

Each accumulated SNR sample is a signed 8-bit integer representing measured_SNR_dB × 4 — i.e., 0.25 dB resolution, range approximately −32 to +31.75 dB. The same convention is used by PACKET_STATS and every place else SNR is carried in this protocol.

2.13.4. Trace Forwarding

When a node receives a TRACE packet with route_type == ROUTE_TYPE_DIRECT:

  1. Parse the payload preamble: tag, auth_code, flags. Extract path_sz = flags & 0x03.
  2. Compute consumed_bytes = header.path_length × (1 << path_sz). This is the byte offset within the payload’s path_hashes trailer of the next hop hash to be checked.
  3. If consumed_bytes ≥ path_hashes_length, the TRACE has consumed every hop: this node is the final destination. Deliver the trace (hashes + SNRs) to the application layer and do not forward.
  4. Otherwise, compare the (1 << path_sz) bytes at path_hashes[consumed_bytes] against this node’s own public-key prefix. If they do not match, drop the packet (this node is not the expected next hop).
  5. If they match, and forwarding policy permits, and the packet’s dedup signature is not already in the “seen” table: a. Append this node’s measured SNR byte ((int8)(measured_snr_dB × 4)) to the header’s path buffer at offset path_length. b. Increment the header’s path_length by 1. c. Retransmit.

Because only the header fields change during forwarding, the payload (including the original path_hashes) remains byte-stable across hops. This is what makes a TRACE packet useful: at the final destination, the receiver can hand both the original intended path and the measured per-hop SNRs to the application for analysis.

2.13.5. Deduplication Caveat

Because TRACE packets mutate their header fields as they traverse the mesh, a naive packet-signature scheme that ignores the header (§2.17.5) would correctly deduplicate each hop. However, the specification in §2.17.5 explicitly includes path_length in the TRACE signature precisely to prevent the same trace from being forwarded twice along overlapping sub-paths (which could occur if the return path revisits an intermediate node). Implementations MUST follow §2.17.5.

2.13.6. Historical Note

Earlier drafts of this specification stated that “SNR samples are in the payload” and described only a flat payload of tag + auth + flags + snr_samples. That description is inconsistent with the reference firmware, which:

The layout documented above reflects the actual wire behaviour of the reference firmware (see Mesh.cpp:41-65, Mesh.cpp:684-698).


2.14. Multipart Payload

Payload type PAYLOAD_TYPE_MULTIPART (0x0A). A composite payload that encapsulates another payload plus a “remaining” counter. Currently used only for multi-ACK bursts.

┌─────────────────────────┬──────────────────────┐
│ remaining_and_subtype   │    sub_payload       │
│         1 byte          │      variable        │
└─────────────────────────┴──────────────────────┘

The leading byte packs:

BitsMaskFieldDescription
0–30x0FSub-typeA payload type value (e.g., ACK).
4–70xF0RemainingNumber of additional bursts expected.

For a multi-ACK:


2.15. Control Payload

Payload type PAYLOAD_TYPE_CONTROL (0x0B). Carries unencrypted control data, typically for discovery.

┌───────┬─────────────────┐
│ flags │      data       │
│  1 B  │    variable     │
└───────┴─────────────────┘

The flags byte has two sub-fields:

BitsMaskFieldDescription
0–30x0FSub-dataSub-type-specific flag bits.
4–70xF0Sub-typeIdentifies the control message sub-type.

Bit 7 of the flags byte (0x80) additionally signals that the control packet is valid only as a zero-hop direct packet; receivers MUST drop it if the packet’s hop count is not zero.

2.15.1. DISCOVER_REQ (sub-type 0x8)

┌───────┬─────────────┬──────┬──────────────┐
│ flags │ type_filter │ tag  │    since     │
│  1 B  │    1 B      │  4 B │  4 B (opt)   │
└───────┴─────────────┴──────┴──────────────┘

2.15.2. DISCOVER_RESP (sub-type 0x9)

┌───────┬──────┬──────┬──────────────────┐
│ flags │ snr  │ tag  │     pubkey       │
│  1 B  │ 1 B  │  4 B │    8 or 32 B     │
└───────┴──────┴──────┴──────────────────┘

2.16. Raw Custom Payload

Payload type PAYLOAD_TYPE_RAW_CUSTOM (0x0F). The payload is opaque to the network layer; the application is responsible for any framing, encryption, and authentication. Raw packets are handled only in direct routing mode.


2.17. Routing and Forwarding Rules

2.17.1. Flood Routing

When a node sends a flood packet:

  1. The route type is set to ROUTE_TYPE_FLOOD (or ROUTE_TYPE_TRANSPORT_FLOOD with appropriate codes).
  2. The sender’s own hash is not appended to path; the hop_count is 0 when transmitted.
  3. The packet’s deduplication signature is recorded locally.
  4. The packet is broadcast.

When a node receives a flood packet:

  1. Validate path_length. Drop if invalid.
  2. Check the deduplication table (§2.18). If already seen, drop.
  3. Process the payload (decrypt/verify/etc.) to the extent the node is able.
  4. If the node’s forwarding policy (§2.17.4) permits forwarding: a. Append this node’s hash to path, incrementing hop_count. b. If appending would cause hop_count × hash_size > MAX_PATH_SIZE, do not forward. c. Schedule a retransmission after a randomized delay based on estimated airtime.

2.17.2. Direct Routing

Direct-routed packets carry an explicit ordered path of hashes. Each forwarder consumes one entry.

When a node sends a direct packet with a known path of length N:

  1. Route type = ROUTE_TYPE_DIRECT (or ROUTE_TYPE_TRANSPORT_DIRECT).
  2. path = the sequence of hashes from the next hop to the final destination, in order.
  3. hop_count = N.

When a node receives a direct packet:

  1. Validate path_length.
  2. If hop_count == 0, this is a zero-hop packet — process only if this node is the intended destination.
  3. If the first entry in path matches this node’s hash: a. Process the payload if addressed to us. b. If forwarding is permitted, remove the first entry from path (shifting subsequent entries left) and decrement hop_count. c. Retransmit.
  4. Otherwise, do not forward (but may still process the payload if addressed to us — e.g., ACKs sent back along a known path).

2.17.3. Zero-Hop Packets

A zero-hop packet is a direct-routed packet with hop_count = 0. It is intended only for neighbours in direct radio range and is never forwarded.

2.17.4. Forwarding Policy

Each node decides locally whether to forward a given packet. Typical policies:

A forwarder MUST respect these hard rules:

2.17.5. Packet Hash (Deduplication Signature)

Every packet has a deduplication signature computed from the payload type and payload content. For most payload types:

signature_input = payload_type_byte || payload
signature       = SHA-256(signature_input)[0..8]   # first 8 bytes, used as 64-bit hash

For PAYLOAD_TYPE_TRACE, the path length is also included:

signature_input = payload_type_byte || path_length_byte || payload

This ensures TRACE packets with different accumulated SNR paths are not falsely deduplicated.


2.18. Duplicate Suppression

A compliant node MUST maintain a “seen packets” table that retains recent packet signatures (§2.17.5) for long enough to suppress duplicate retransmissions across the mesh.

Recommended policy:

Duplicate suppression is REQUIRED to prevent broadcast storms during flood routing.


Appendix A: Compliance Checklist

A minimally compliant implementation of The Protocol MUST:

  1. Parse and emit packets per §2.1–§2.6.
  2. Validate path_length per §2.5.2 and drop invalid packets.
  3. Respect MAX_PACKET_PAYLOAD and MAX_TRANS_UNIT.
  4. Implement deduplication per §2.18.
  5. Implement Ed25519 signature verification for adverts per §2.8.1.
  6. Implement X25519 key agreement, AES-128-ECB encryption, HMAC-SHA256 MAC per §2.7.
  7. Use PUB_KEY_SIZE = 32 bytes for the HMAC key and the first 16 bytes for the AES key (§2.7.4).
  8. Treat 0xFF as an on-wire invalid header and drop such packets (§2.2.2).

Compliance requirements for the Companion Protocol are defined in the companion document (Part 2: The Companion Protocol, Appendix A).


Appendix B: Constants Summary

B.1. Size Constants

ConstantValueUnits
PUB_KEY_SIZE32bytes
PRV_KEY_SIZE64bytes
SEED_SIZE32bytes
SIGNATURE_SIZE64bytes
CIPHER_KEY_SIZE16bytes
CIPHER_BLOCK_SIZE16bytes
CIPHER_MAC_SIZE2bytes
MAX_ADVERT_DATA_SIZE32bytes
MAX_PACKET_PAYLOAD184bytes
MAX_PATH_SIZE64bytes
MAX_TRANS_UNIT255bytes
MAX_GROUP_DATA_LENGTH165bytes
MAX_TEXT_LEN160bytes

The Companion Protocol’s default MAX_FRAME_SIZE (172 bytes) is defined in the companion document (Part 2, Appendix B).

B.2. Route Types

ValueName
0x00ROUTE_TYPE_TRANSPORT_FLOOD
0x01ROUTE_TYPE_FLOOD
0x02ROUTE_TYPE_DIRECT
0x03ROUTE_TYPE_TRANSPORT_DIRECT

B.3. Payload Types

ValueName
0x00PAYLOAD_TYPE_REQ
0x01PAYLOAD_TYPE_RESPONSE
0x02PAYLOAD_TYPE_TXT_MSG
0x03PAYLOAD_TYPE_ACK
0x04PAYLOAD_TYPE_ADVERT
0x05PAYLOAD_TYPE_GRP_TXT
0x06PAYLOAD_TYPE_GRP_DATA
0x07PAYLOAD_TYPE_ANON_REQ
0x08PAYLOAD_TYPE_PATH
0x09PAYLOAD_TYPE_TRACE
0x0APAYLOAD_TYPE_MULTIPART
0x0BPAYLOAD_TYPE_CONTROL
0x0FPAYLOAD_TYPE_RAW_CUSTOM

B.4. Advert Node Types

ValueName
0x00ADV_TYPE_NONE
0x01ADV_TYPE_CHAT
0x02ADV_TYPE_REPEATER
0x03ADV_TYPE_ROOM
0x04ADV_TYPE_SENSOR

B.5. Text Types

ValueName
0x00TXT_TYPE_PLAIN
0x01TXT_TYPE_CLI_DATA
0x02TXT_TYPE_SIGNED_PLAIN

Companion Protocol command and response codes are tabulated in the companion document (Part 2, Appendix B).


Appendix C: Known Discrepancies Between Documentation and Implementation

This specification reflects the actual on-wire behaviour of a compliant implementation, not prior Core documentation drafts. The following discrepancies were identified and resolved in favour of the implementation:

C.1. Advert Flags Byte (§2.8.3)

Earlier documentation listed the values 0x01 (chat), 0x02 (repeater), 0x03 (room), 0x04 (sensor) alongside the bit-flag masks 0x10, 0x20, 0x40, 0x80 in a single “flags” table, implying they occupied the same field. In reality, the node type occupies the low 4 bits of the flags byte, and the presence flags occupy the high 4 bits. The type and presence flags are decoded independently. This specification clarifies the split.

C.2. HMAC Key Length (§2.7.4)

The HMAC key uses the full 32-byte shared secret, even though the AES key uses only the first 16 bytes of that same secret. A reader of the existing MeshCore docs might reasonably assume a 16-byte HMAC key by symmetry; the implementation does not. Incorrectly truncating the HMAC key to 16 bytes produces MAC verification failures that are hard to diagnose.

C.3. TRACE Packet Wire Layout

Most payload types carry their routing path in the packet header’s path field. TRACE packets do not: the header’s path field is repurposed as an accumulator of measured per-hop SNR bytes, and the original path-hash sequence lives in the payload trailer (after the 9-byte tag/auth/flags preamble). Additionally, the header’s path_length byte is reinterpreted as a hop-consumption counter (its low 6 bits) and the top 2 bits (normally the hash-size code of §2.5) are locked at zero — the TRACE hash size is carried separately in the payload’s flags byte using a distinct encoding (1 << bits → 1/2/4 bytes per hash).

This is substantially different from every other payload type and requires special-case handling by forwarders. See §2.13 for the full specification.

Earlier drafts of this specification described TRACE packets as carrying their SNR samples in the payload. That description does not match the reference firmware; it has been replaced.

C.4. Protocol Version v1 Only

Payload versions 2–4 (header bits 6–7) are reserved but not in use. All current implementations emit and expect version 1.

C.5. ACK Hash Algorithm (§2.12)

Earlier drafts described PAYLOAD_TYPE_ACK as carrying a 4-byte little-endian CRC-32 of the acknowledged message. This was incorrect. The reference firmware (BaseChatMesh.cpp) computes the field as the first 4 bytes of SHA-256 over a message-type-dependent buffer that includes the recipient’s public key as a salt, and transmits those 4 bytes directly (no additional byte-order transformation).

The field name checksum in some firmware comments is a legacy term; it is not a CRC in any standard sense. Implementations of the original CRC-32 description will never produce a matching ACK.

Companion Protocol discrepancies (error codes, stats frames) are documented in the companion document (Part 2, Appendix C).