Files
meshcore-spec/versions/v0.1.0/01-wire-format.md
T
swaitsandClaude Opus 4.7 9232c16f7c
mdBook / build (push) Canceled after 0s
mdBook / deploy (push) Canceled after 0s
feat(versioning): snapshot corpus per spec version
A frozen spec is only meaningful if it's paired with the corpus
that was current at release time — otherwise a v0.1.0 reader
follows "see corpus/payloads/ack/" and lands on a file whose
test vectors may have changed since v0.1.0 was cut.

Three changes:

1. `cut-version` now copies BOTH src/ AND corpus/ into the
   snapshot directory. A release is the pair, not just the text.

2. The corpus URL rewriting in cut-version now retargets
   /tree/main/corpus -> /tree/main/versions/<v>/corpus (and the
   blob variant). This lets the link resolve on GitHub from main
   alone — no git tag required, since the frozen copy is a real
   path under main. Downgrades the "tag is REQUIRED" message from
   last commit back to "optionally tag the commit".

3. The frozen corpus's spec_ref fields get rewritten too: from
   `src/04-payload-ack.md` to `versions/<v>/04-payload-ack.md`,
   so a reader following a test vector lands on the spec text the
   vector was paired with at release time.

versions/v0.1.0/ regenerated from scratch via `just cut-version v0.1.0`
to pick up the corpus snapshot + the new sed patterns. The release
is now self-consistent: v0.1.0 spec links go to v0.1.0 corpus, which
links back to v0.1.0 spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:38:02 -06:00

141 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MeshCore Protocol Specification
## Section 1: Wire Format
### Overview
A MeshCore packet is the fundamental transmission unit. It consists of a 1-byte
header, an optional 4-byte transport codes field, a 1-byte path length, a
variable-length path, and a variable-length payload. The maximum packet size on
the wire is 255 bytes (MAX_TRANS_UNIT).
### Packet Layout
```
0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Header | Transport Codes (opt, 4B) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Path Length | Path (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The fields appear in this order on the wire:
```
[header(1)][transport_codes(4)?][path_len(1)][path(0-64)][payload(1-184)]
```
### Field Summary
| Field | Size | Required | Description |
|-------|------|----------|-------------|
| Header | 1 byte | Yes | Route type, payload type, protocol version (see [Section 2](02-header.md)) |
| Transport Codes | 4 bytes | Conditional | Two uint16_le values; present only when route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03) |
| Path Length | 1 byte | Yes | Encoded hash count and hash size (see [Section 3](03-path.md)) |
| Path | 0-64 bytes | Yes | Sequence of node hashes; length = hash_count × hash_size |
| Payload | 1-184 bytes | Yes | Packet data; structure depends on payload type |
### Transport Codes
Transport codes are present if and only if the route type (header bits 0-1) is
`TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03).
When present, transport codes consist of two little-endian 16-bit unsigned
integers, for a total of 4 bytes:
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code 1 (uint16_le) | Code 2 (uint16_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
- **Code 1**: Calculated from regional scope parameters
- **Code 2**: Reserved (typically 0)
When transport codes are not present (route types `FLOOD` and `DIRECT`), the
implementation MUST set both transport code values to zero internally.
### Encoding Algorithm
To encode a packet to wire format:
1. Write the header byte.
2. If the route type is `TRANSPORT_FLOOD` or `TRANSPORT_DIRECT`:
a. Write transport_codes[0] as uint16_le (2 bytes).
b. Write transport_codes[1] as uint16_le (2 bytes).
3. Write the path_len byte.
4. Calculate path byte length as `hash_count × hash_size` (see [Section 3](03-path.md)).
5. Write `path_byte_length` bytes from the path array.
6. Write `payload_len` bytes from the payload array.
The total wire length is:
```
1 + (hasTransportCodes ? 4 : 0) + 1 + path_byte_length + payload_len
```
This MUST NOT exceed MAX_TRANS_UNIT (255) bytes.
### Decoding Algorithm
To decode a packet from wire format:
1. Read 1 byte as the header. Extract route type from bits 0-1.
2. If route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03):
a. Read 2 bytes as transport_codes[0] (uint16_le).
b. Read 2 bytes as transport_codes[1] (uint16_le).
Otherwise, set both transport codes to zero.
3. Read 1 byte as path_len.
4. Validate path_len (see [Section 3](03-path.md)):
a. Extract hash_size = (path_len >> 6) + 1. If hash_size is 4, the packet
is INVALID.
b. Extract hash_count = path_len & 63.
c. If hash_count × hash_size > MAX_PATH_SIZE (64), the packet is INVALID.
5. Calculate path_byte_length = hash_count × hash_size.
6. Read path_byte_length bytes as the path.
7. Let `i` be the current read position. If `i >= total_length`, the packet is
INVALID (payload MUST contain at least 1 byte).
8. Calculate payload_len = total_length - i.
9. If payload_len > MAX_PACKET_PAYLOAD (184), the packet is INVALID.
10. Read payload_len bytes as the payload.
### Size Constraints
| Constraint | Value | Enforcement |
|-----------|-------|-------------|
| Maximum wire length | 255 bytes | Encoder MUST NOT produce packets exceeding this |
| Maximum payload | 184 bytes | Decoder MUST reject payloads exceeding this |
| Maximum path | 64 bytes | Decoder MUST reject paths exceeding this |
| Minimum payload | 1 byte | Decoder MUST reject packets with zero-length payload |
| Minimum packet | 3 bytes | Header (1) + path_len (1) + payload (1 minimum) |
### Error Conditions
A conforming decoder MUST reject packets with any of the following:
1. Total wire length less than 3 bytes (no room for header + path_len + payload)
2. Total wire length less than minimum required for the route type:
- 3 bytes for FLOOD or DIRECT
- 7 bytes for TRANSPORT_FLOOD or TRANSPORT_DIRECT (adds 4 transport code bytes)
3. Invalid path_len encoding (see [Section 3](03-path.md))
4. Path byte length exceeds remaining bytes
5. Zero-length payload after header, transport codes, path_len, and path
6. Payload length exceeding MAX_PACKET_PAYLOAD (184)
### Cross-References
- [Section 2: Header](02-header.md) — Header byte encoding details
- [Section 3: Path](03-path.md) — Path length encoding and path field
- Test vectors: [`corpus/wire-format/framing/`](https://github.com/swaits/meshcore-spec/tree/main/versions/v0.1.0/corpus/wire-format/framing/), [`corpus/wire-format/invalid/`](https://github.com/swaits/meshcore-spec/tree/main/versions/v0.1.0/corpus/wire-format/invalid/)
### Reference Implementation
- `Packet::writeTo()` in `src/Packet.cpp` — Encoding
- `Packet::readFrom()` in `src/Packet.cpp` — Decoding
- `Packet::getRawLength()` in `src/Packet.cpp` — Wire length calculation