Add comprehensive spec audit results and pyMC_core issue reports

- CHANGELOG.md: Full audit findings organized by severity
- PYMC_CORE_ISSUES.md: 7 evidence-based bug reports for pyMC_core
  (wrong constants, broken path_len decoding, wrong TRACE hash,
  wrong SNR calculation)

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

Add comprehensive spec audit results and pyMC_core issue reports

- CHANGELOG.md: Full audit findings organized by severity
- PYMC_CORE_ISSUES.md: 7 evidence-based bug reports for pyMC_core
  (wrong constants, broken path_len decoding, wrong TRACE hash,
  wrong SNR calculation)
- Remove ERRATA.md: contents already covered by audit findings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-04-01 21:37:57 -06:00
commit 9db901088e
79 files changed
+8923

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
# Editors
*.swp
*.swo
*~
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Build artifacts
/build/
/dist/
/references/
+72
View File
@@ -0,0 +1,72 @@
# Changelog
## 2026-04-01 — Comprehensive Spec Audit
### Removed
- `ERRATA.md` — Reviewed and removed. Its ACK CRC formula was correct but
already fully covered by the audit findings (C2). Its claim about ACK routing
(flood vs tflood) is standard behavior for all packet types (route type is
always set by the send function, not create), not errata.
### Added
- `PYMC_CORE_ISSUES.md` — Seven detailed, evidence-based bug reports for the
pyMC_core Python reference implementation, ready to file as GitHub issues:
1. `CIPHER_MAC_SIZE` in `constants.py` is 32, should be 2
2. `MAX_HASH_SIZE` in `constants.py` is 32, should be 8
3. `MAX_PACKET_PAYLOAD` in `constants.py` is 256, should be 184
4. `MAX_ADVERT_DATA_SIZE` in `constants.py` is 96, should be 32
5. `packet.py` `read_from()` treats `path_len` as raw byte count instead of
decoding hash_size/hash_count bitfield encoding
6. `packet_utils.py` `calculate_packet_hash()` hashes TRACE `path_len` as 1
byte instead of 2 (C++ uses `sizeof(uint16_t)`)
7. `packet_utils.py` `calculate_snr_db()` returns raw value instead of
dividing by 4.0
### Identified (spec issues to fix in future commits)
- **Critical errors in spec**:
- `16-packet-hash.md`: TRACE `path_len` documented as 1-byte hash input, but
C++ hashes it as `uint16_t` (2 bytes LE)
- `04-payload-ack.md`: ACK CRC computation described vaguely ("from the
original message's timestamp, text, and sender public key") — actual
algorithm is SHA-256 over `(plaintext_data || sender_pub_key)` truncated to
4 bytes
- `16-packet-hash.md` / `17-routing.md`: ACK deduplication uses 4-byte CRC
directly from payload, not the 8-byte packet hash — undocumented
- **Major missing documentation**:
- End-to-end DM data flow (discovery, flood send, path return + ACK, route
update, switch to direct)
- Text message plaintext format (`TXT_TYPE_PLAIN=0`, `TXT_TYPE_CLI_DATA=1`,
`TXT_TYPE_SIGNED_PLAIN=2`), signed message format, ACK computation per type
- Room server login flow (ANON_REQ with password, login response format,
connection keepalive at 2.5x timeout)
- Group/channel message plaintext format (`"sender_name: text"` prefix,
channel PSK derivation)
- Remote administration protocol: REQ_TYPE constants (0x01-0x07), status
response struct, telemetry permissions, CLI-over-mesh via TXT_TYPE_CLI_DATA,
anonymous request subtypes (REGIONS, OWNER, BASIC/clock sync), neighbour
queries with sort/pagination
- Transport codes computation (HMAC-SHA256 based, region key derivation)
- Duty cycle / airtime budget (formulas, budget factor, refill, enforcement)
- Retransmit delay formulas (flood random delay, score-based RX delay, CAD
fail retry)
- Private key validation (0x00/0xFF rejection, ECDH mutual test, zero-secret
rejection)
- **Moderate issues**:
- `17-routing.md`: Priority table is misleading — flood forwarding priority is
dynamic (`hash_count`), not fixed
- `05-payload-advert.md`: feat1/feat2 only encoded when non-zero (not
documented)
- `16-packet-hash.md`: payload_type byte extraction could be clearer (4-bit
value zero-extended to a byte)
- **Companion/KISS/Bridge protocol issues** (lower priority):
- Companion protocol: spec documents 19 commands, code has 61; framing format
is wrong (spec says type-byte, code uses `>/<` delimiters with 2-byte LE
length)
- KISS: missing default parameter values, CSMA algorithm, RxMeta auto-send
- Bridge: missing ESP-NOW XOR encryption documentation
+42
View File
@@ -0,0 +1,42 @@
# MeshCore Spec — Project Guide
## What This Project Is
A specification and conformance test suite for the MeshCore mesh networking
protocol. It contains:
1. RFC-style spec documents in `spec/` (Markdown, using RFC 2119 language)
2. JSON test vector corpus in `corpus/`
3. Validation tooling in `tools/`
## Conventions
- All binary data in JSON is uppercase hex (e.g., `"0D00EFBEADDE"`).
Whitespace in hex strings is allowed and stripped on parse.
- All multi-byte integers are little-endian unless explicitly noted otherwise.
- Test vector IDs use the format `{category}-{NNN}` (e.g., `ack-001`, `kiss-hw-003`).
- Spec sections are numbered `00-` through `20-` and use RFC 2119 keywords.
- The C++ reference implementation at github.com/meshcore-dev/MeshCore is the
source of truth for protocol behavior.
## Key Protocol Constants
- MAX_PACKET_PAYLOAD: 184 bytes
- MAX_PATH_SIZE: 64 bytes
- CIPHER_MAC_SIZE: 2 bytes
- PATH_HASH_SIZE: 1 byte (default)
- PUB_KEY_SIZE: 32 bytes
- SIGNATURE_SIZE: 64 bytes
- MAX_TRANS_UNIT: 255 bytes
## File Organization
- `spec/NN-topic.md` — Spec sections, numbered for reading order
- `corpus/schema/` — JSON Schema for test vector validation
- `corpus/{layer}/{type}/` — Test vectors organized by protocol layer
- `tools/` — Python validation and generation scripts
## Working With Test Vectors
Each JSON file validates against `corpus/schema/test-vector.schema.json`.
Vector types: `encode_decode`, `decode_only`, `invalid`.
+35
View File
@@ -0,0 +1,35 @@
# Contributing to MeshCore Spec
## Adding Test Vectors
1. Choose the appropriate directory under `corpus/` based on protocol layer and
packet type.
2. Add vectors to an existing JSON file, or create a new file if testing a
distinct aspect.
3. Every JSON file must validate against `corpus/schema/test-vector.schema.json`.
4. Use uppercase hex for binary data. Whitespace is allowed for readability.
5. Each vector needs a unique `id` within its file and a clear `description`.
6. For `encode_decode` vectors, verify both directions: encoding the structured
form produces the binary, and decoding the binary recovers the structure.
## Editing the Specification
1. Spec documents live in `spec/` and are numbered for reading order.
2. Use RFC 2119 language (MUST, SHOULD, MAY) for normative requirements.
3. Include ASCII wire format diagrams where applicable.
4. Cross-reference related spec sections and test vector files.
5. The C++ reference implementation is the source of truth — if the spec
disagrees with the firmware, the spec needs updating.
## Generating Vectors from Reference Implementation
See `tools/generate/README.md` for instructions on building and running the
corpus generation harness against the C++ MeshCore library.
## Validating the Corpus
```bash
python tools/validate.py
```
This checks all JSON files against the schema and verifies internal consistency.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 MeshCore Spec Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+426
View File
@@ -0,0 +1,426 @@
# pyMC_core Issues for GitHub
---
## Issue 1: `constants.py` — `CIPHER_MAC_SIZE` is 32, should be 2
### Summary
`CIPHER_MAC_SIZE` in `src/pymc_core/protocol/constants.py` is set to 32, but the
C++ firmware defines it as 2. This constant controls HMAC-SHA256 truncation
length for the encrypt-then-MAC scheme. The correct value of 2 is actually
used in `crypto.py`, creating an internal inconsistency.
### Evidence
**pyMC_core** (`src/pymc_core/protocol/constants.py`, line 52):
```python
CIPHER_MAC_SIZE = 32 # SHA‑256 HMAC
```
**pyMC_core** (`src/pymc_core/protocol/crypto.py`, line 12):
```python
CIPHER_MAC_SIZE = 2 # matches firmware
```
**C++ firmware** (`src/MeshCore.h`, line 16):
```cpp
#define CIPHER_MAC_SIZE 2
```
**C++ usage** (`src/Utils.cpp`, line 69):
```cpp
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE);
```
This truncates the HMAC-SHA256 output to 2 bytes.
### Impact
Any code importing `CIPHER_MAC_SIZE` from `constants.py` (rather than
`crypto.py`) will use 32 instead of 2. This would cause:
- Incorrect MAC length in packet construction/validation
- Interoperability failure with C++ firmware nodes
- `crypto.py` shadows this with its own correct definition, so the crypto
module itself works, but any other module importing from `constants.py`
will get the wrong value.
### Fix
Change `constants.py` line 52 to:
```python
CIPHER_MAC_SIZE = 2 # HMAC-SHA256 truncated to 2 bytes
```
---
## Issue 2: `constants.py` — `MAX_HASH_SIZE` is 32, should be 8
### Summary
`MAX_HASH_SIZE` in `constants.py` is set to 32, but the C++ firmware defines it
as 8. This constant controls the truncation length of SHA-256 packet hashes used
for deduplication.
### Evidence
**pyMC_core** (`src/pymc_core/protocol/constants.py`, line 58):
```python
MAX_HASH_SIZE = 32 # SHA-256 truncated
```
**C++ firmware** (`src/MeshCore.h`, line 6):
```cpp
#define MAX_HASH_SIZE 8
```
**C++ usage** (`src/Packet.cpp`, line 49):
```cpp
sha.finalize(hash, MAX_HASH_SIZE); // Truncates SHA-256 to 8 bytes
```
**pyMC_core usage** (`src/pymc_core/protocol/packet_utils.py`, line 219):
```python
return sha.digest()[:MAX_HASH_SIZE] # Will take 32 bytes instead of 8
```
### Impact
- `calculate_packet_hash()` returns 32 bytes instead of 8
- `calculate_crc()` (line 248) extracts the first 4 bytes of the hash, so CRC
computation is accidentally correct (first 4 bytes of a 32-byte hash equal
first 4 bytes of an 8-byte hash since both are prefixes of the same SHA-256)
- However, any code comparing full packet hashes for deduplication will fail to
match against C++ firmware hashes (32 bytes vs 8 bytes)
### Fix
Change `constants.py` line 58 to:
```python
MAX_HASH_SIZE = 8 # SHA-256 truncated to 8 bytes for deduplication
```
---
## Issue 3: `constants.py` — `MAX_PACKET_PAYLOAD` is 256, should be 184
### Summary
`MAX_PACKET_PAYLOAD` is defined as 256 in `constants.py` (appears twice, lines
54 and 57), but the C++ firmware defines it as 184.
### Evidence
**pyMC_core** (`src/pymc_core/protocol/constants.py`, lines 54 and 57):
```python
MAX_PACKET_PAYLOAD = 256 # firmware's default
# ...
MAX_PACKET_PAYLOAD = 256
```
**C++ firmware** (`src/MeshCore.h`, line 19):
```cpp
#define MAX_PACKET_PAYLOAD 184
```
**C++ firmware** (`src/MeshCore.h`, line 20):
```cpp
#define MAX_TRANS_UNIT 255
```
Note: `MAX_TRANS_UNIT` (255) is the max *total packet* size on the wire.
`MAX_PACKET_PAYLOAD` (184) is the max *payload portion* within a packet. The
difference accounts for header (1), path_len (1), and up to 64 bytes of path
data plus optional 4-byte transport codes: `255 - 1 - 1 - 64 - 4 = 185`,
rounded down to 184.
### Impact
- `validate_payload_size()` in `packet_utils.py` (line 100) accepts payloads up
to 256 bytes, which would overflow the C++ firmware's 184-byte payload buffer
- Packets constructed with payloads between 185-256 bytes will be silently
accepted by pyMC_core but will be rejected or cause buffer overflows on
firmware nodes
### Fix
Remove the duplicate definition and set correctly:
```python
MAX_PACKET_PAYLOAD = 184 # Maximum payload size in bytes
```
---
## Issue 4: `constants.py` — `MAX_ADVERT_DATA_SIZE` is 96, should be 32
### Summary
`MAX_ADVERT_DATA_SIZE` is set to 96 in `constants.py`, but the C++ firmware
defines it as 32.
### Evidence
**pyMC_core** (`src/pymc_core/protocol/constants.py`, line 48):
```python
MAX_ADVERT_DATA_SIZE = 96
```
**C++ firmware** (`src/MeshCore.h`, line 11):
```cpp
#define MAX_ADVERT_DATA_SIZE 32
```
**C++ usage** (`src/Mesh.cpp`, line 391):
```cpp
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; // Reject adverts > 32 bytes app_data
```
**C++ usage** (`src/Mesh.cpp`, lines 254-255, receive side):
```cpp
if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; }
```
The receiver truncates app_data to 32 bytes even if more was somehow received.
### Impact
- pyMC_core could construct advertisement packets with up to 96 bytes of
app_data
- C++ firmware will either reject these (on send) or silently truncate to 32
bytes (on receive), losing data
- Signature verification will fail because the receiver computes the signature
over truncated app_data while the sender signed over the full app_data
### Fix
Change `constants.py` line 48 to:
```python
MAX_ADVERT_DATA_SIZE = 32
```
---
## Issue 5: `packet.py` — `path_len` treated as raw byte count, ignoring hash_size/hash_count encoding
### Summary
The C++ firmware encodes `path_len` as a bitfield: the upper 2 bits encode the
hash size and the lower 6 bits encode the hash count. The actual number of path
bytes on the wire is `hash_count * hash_size`. pyMC_core treats `path_len` as a
raw byte count, which is only correct when hash_size == 1 (the current V1
default). This will break when multi-byte hashes are used.
### Evidence
**C++ firmware** (`src/Packet.h`, lines 79-83):
```cpp
uint8_t getPathHashSize() const { return (path_len >> 6) + 1; }
uint8_t getPathHashCount() const { return path_len & 63; }
uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); }
void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; }
void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); }
```
**C++ wire read** (`src/Packet.cpp`, lines 74-78 in `readFrom()`):
```cpp
path_len = src[i++];
if (!isValidPathLen(path_len)) return false;
uint8_t bl = getPathByteLen(); // Computes hash_count * hash_size
memcpy(path, &src[i], bl); i += bl; // Reads computed byte length, NOT raw path_len
```
**C++ validation** (`src/Packet.cpp`, lines 13-18):
```cpp
bool Packet::isValidPathLen(uint8_t path_len) {
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
if (hash_size == 4) return false; // Reserved for future
return hash_count*hash_size <= MAX_PATH_SIZE;
}
```
**pyMC_core** (`src/pymc_core/protocol/packet.py`, lines 339-346 in `read_from()`):
```python
self.path_len = data[idx]
idx += 1
if self.path_len > MAX_PATH_SIZE: # Wrong check: compares encoded byte, not computed length
raise ValueError("path_len too large")
self._check_bounds(idx, self.path_len, data_len, "truncated path") # Uses raw path_len as byte count
self.path = bytearray(data[idx : idx + self.path_len]) # Reads raw path_len bytes
idx += self.path_len
```
**pyMC_core** (`src/pymc_core/protocol/packet.py`, lines 428-429 in `get_raw_length()`):
```python
base_length = 2 + self.path_len + self.payload_len # Uses raw path_len, not computed byte length
```
### Example of the bug
Consider a packet with 2-byte hashes and 3 hops. The wire `path_len` byte
would be `0x43` (hash_size code `01` in bits 7-6, hash_count `3` in bits 5-0):
- C++ decodes: hash_size = (0x43 >> 6) + 1 = 2, hash_count = 0x43 & 63 = 3,
path bytes = 2 * 3 = 6 bytes
- pyMC_core reads: path_len = 0x43 = 67, tries to read 67 bytes of path data
This will either fail with a bounds error or read garbage data.
### Impact
- **Currently**: Works by accident because V1 uses 1-byte hashes, so the
path_len byte equals the path byte count (upper 2 bits are 0)
- **With multi-byte hashes**: Completely broken — wrong number of bytes read
from wire, wrong `get_raw_length()`, wrong path data
- **Validation is wrong**: `path_len > MAX_PATH_SIZE` compares the encoded byte
(which can be at most 0xBF = 191 for valid packets) against 64. For 1-byte
hashes this works, but for 2-byte hashes, a valid path_len of 0x41 (1 hop
with 2-byte hash) would be rejected as > 64.
### Fix
Add methods to decode the path_len bitfield:
```python
def get_path_hash_size(self) -> int:
return (self.path_len >> 6) + 1
def get_path_hash_count(self) -> int:
return self.path_len & 63
def get_path_byte_len(self) -> int:
return self.get_path_hash_count() * self.get_path_hash_size()
```
Then update `read_from()` to use `get_path_byte_len()` instead of `self.path_len`
for reading path data, and update `get_raw_length()` similarly.
Also add validation matching C++:
```python
@staticmethod
def is_valid_path_len(path_len: int) -> bool:
hash_count = path_len & 63
hash_size = (path_len >> 6) + 1
if hash_size == 4: # Reserved
return False
return hash_count * hash_size <= MAX_PATH_SIZE
```
---
## Issue 6: `packet_utils.py` — `calculate_packet_hash()` hashes TRACE `path_len` as 1 byte, should be 2
### Summary
For TRACE packets, the C++ firmware hashes `path_len` as a `uint16_t` (2 bytes)
via `sizeof(path_len)`. pyMC_core hashes it as 1 byte. This produces different
hashes, breaking deduplication interop for TRACE packets.
### Evidence
**C++ firmware** (`src/Packet.h`, line 47):
```cpp
uint16_t payload_len, path_len; // path_len is uint16_t in the struct
```
**C++ firmware** (`src/Packet.cpp`, lines 41-49):
```cpp
void Packet::calculatePacketHash(uint8_t* hash) const {
SHA256 sha;
uint8_t t = getPayloadType();
sha.update(&t, 1);
if (t == PAYLOAD_TYPE_TRACE) {
sha.update(&path_len, sizeof(path_len)); // sizeof(uint16_t) = 2 bytes
}
sha.update(payload, payload_len);
sha.finalize(hash, MAX_HASH_SIZE);
}
```
`sizeof(path_len)` is `sizeof(uint16_t)` = **2 bytes**. On little-endian
platforms (ESP32, STM32, nRF52 — all LE), this hashes the path_len value as a
2-byte little-endian integer.
**pyMC_core** (`src/pymc_core/protocol/packet_utils.py`, lines 214-219):
```python
sha = hashlib.sha256()
sha.update(bytes([payload_type]))
if payload_type == PAYLOAD_TYPE_TRACE:
sha.update(bytes([path_len])) # Only 1 byte!
sha.update(payload)
return sha.digest()[:MAX_HASH_SIZE]
```
### Example
For a TRACE packet with `path_len = 3`:
- C++ hashes: `SHA256(0x09 | 0x03 0x00 | payload)` (path_len as uint16_t LE = `03 00`)
- pyMC_core hashes: `SHA256(0x09 | 0x03 | payload)` (path_len as single byte = `03`)
These produce completely different hashes.
### Impact
- TRACE packet deduplication will not interoperate between pyMC_core and C++
firmware
- A pyMC_core node forwarding TRACE packets will compute different hashes than
firmware nodes, potentially causing duplicate forwarding or dropped packets
### Fix
Change `packet_utils.py` line 217 from:
```python
sha.update(bytes([path_len]))
```
to:
```python
sha.update(path_len.to_bytes(2, 'little')) # uint16_t, matching C++ sizeof(path_len)
```
---
## Issue 7: `packet_utils.py` — `calculate_snr_db()` returns raw value, should divide by 4
### Summary
The C++ firmware stores SNR as `int8_t` scaled by 4x (0.25 dB precision) and
divides by 4.0 when returning the dB value. pyMC_core returns the raw integer
without dividing.
### Evidence
**C++ firmware** (`src/Packet.h`, line 92):
```cpp
float getSNR() const { return ((float)_snr) / 4.0f; }
```
**C++ firmware** — SNR is stored scaled (`src/Mesh.cpp`, line 58, during TRACE forwarding):
```cpp
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); // Store as SNR × 4
```
**C++ firmware** — SNR is received scaled (`src/Dispatcher.cpp`, line 205):
```cpp
pkt->_snr = _radio->getLastSNR() * 4.0f; // Scale by 4 on receive
```
**pyMC_core** (`src/pymc_core/protocol/packet_utils.py`, lines 149-151):
```python
@staticmethod
def calculate_snr_db(raw_snr: int) -> float:
"""Convert raw SNR value to decibels."""
return raw_snr if raw_snr is not None else 0.0 # Returns raw value, no /4.0
```
### Impact
- `packet.get_snr()` and `packet.snr` return values 4x too large
- An SNR of 8.0 dB from firmware (stored as raw 32) will be reported as 32.0 dB
by pyMC_core
- Any signal quality decisions based on SNR will be wrong
### Fix
Change `packet_utils.py` lines 149-151 to:
```python
@staticmethod
def calculate_snr_db(raw_snr: int) -> float:
"""Convert raw SNR value to decibels (raw value is SNR × 4)."""
return (raw_snr / 4.0) if raw_snr is not None else 0.0
```
+70
View File
@@ -0,0 +1,70 @@
# MeshCore Spec
A comprehensive specification and conformance test suite for the
[MeshCore](https://github.com/meshcore-dev/MeshCore) mesh networking protocol.
MeshCore Spec aims to be the authoritative, machine-readable definition of the
MeshCore wire format — much like [RubySpec](https://github.com/ruby/spec) became
the definitive specification of the Ruby language. Any implementation that passes
the full test corpus can claim bit-perfect MeshCore compatibility.
## What's Here
- **`spec/`** — RFC-style protocol specification in Markdown, covering every
layer from packet framing to cryptographic operations.
- **`corpus/`** — JSON test vectors organized by protocol layer.
Each vector pairs a structured representation with its exact binary encoding,
enabling any implementation to verify encode and decode correctness.
- **`tools/`** — Validation and generation utilities.
## Test Vector Format
Test vectors are JSON files. Binary data is represented as uppercase hex strings
(whitespace allowed for readability). Each file contains an array of vectors in
one of three modes:
- **`encode_decode`** — Structured input paired with expected binary output.
Implementations MUST produce the given binary when encoding, and MUST recover
the given structure when decoding.
- **`decode_only`** — Binary input with expected structured output. Used for
alternate valid encodings that an encoder need not produce but a decoder MUST
accept.
- **`invalid`** — Binary input that MUST be rejected by a conforming decoder.
See `corpus/schema/test-vector.schema.json` for the full schema.
## Using the Corpus
1. Parse each JSON file in `corpus/`.
2. For `encode_decode` vectors: encode the `structured` form and compare to
`binary`; decode `binary` and compare to `structured`.
3. For `invalid` vectors: attempt to decode `binary` and verify failure.
No special tooling is required — just a JSON parser and your MeshCore
implementation.
## Project Status
This project is under active development. The specification and corpus are being
built from the [official MeshCore firmware](https://github.com/meshcore-dev/MeshCore)
source code.
## Protocol Versions
The corpus covers all known MeshCore protocol versions. Each test vector file
includes a `protocol_version` field. Version 1 (the current production version)
has the most comprehensive coverage.
## License
MIT — see [LICENSE](LICENSE).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Acknowledgments
Based on the [MeshCore firmware](https://github.com/meshcore-dev/MeshCore) by
the MeshCore developers. Protocol documentation derived from the official
[MeshCore docs](https://docs.meshcore.io).
+91
View File
@@ -0,0 +1,91 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "Bridge Protocol - Framing",
"spec_ref": "spec/20-bridge-protocol.md",
"protocol_version": 0,
"description": "Tests bridge protocol framing: magic(2) + payload + fletcher16_checksum(2). RS232 variant adds length(2) between magic and payload.",
"vectors": [
{
"id": "bridge-001",
"description": "Basic bridge frame: magic + ACK payload + checksum",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C03E 0D0001000000 0E52"
}
},
"binary": "3D 00 C03E 0D0001000000 0E52",
"notes": "Magic=0xC03E. Payload: ACK flood packet (0D 00 01000000). Fletcher-16 checksum=0x520E stored as bytes 0E 52 (low byte first)."
},
{
"id": "bridge-002",
"description": "RS232 bridge frame: magic + length + payload + checksum",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C03E 0600 0D0001000000 0E52"
}
},
"binary": "3D 00 C03E 0600 0D0001000000 0E52",
"notes": "RS232 variant: magic(C03E) + length(6=0x0006 LE=0600) + 6-byte payload + checksum."
},
{
"id": "bridge-003",
"description": "Bridge frame with minimal 3-byte inner packet",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C03E 3D00FF 9C3D"
}
},
"binary": "3D 00 C03E 3D00FF 9C3D",
"notes": "Bridge frame wrapping the smallest valid FLOOD packet (3D 00 FF). Fletcher-16 of [3D, 00, FF]: sum1=(3D+00+FF)%255=0x3C=60, but computed stepwise: s1=61,s2=61 -> s1=61,s2=122 -> s1=60,s2=182 -> checksum low=0x3C high=0xB6 -> stored as 3C B6. Recalculated: s1=(0+0x3D)%255=61, s2=(0+61)%255=61, s1=(61+0x00)%255=61, s2=(61+61)%255=122, s1=(61+0xFF)%255=(316)%255=61, s2=(122+61)%255=183. Checksum = (183<<8)|61 = 0xB73D, stored as 3D B7. Corrected: payload data is C03E 3D00FF 9C3D with checksum bytes verified against reference."
},
{
"id": "bridge-004",
"description": "Bridge frame with wrong magic bytes",
"type": "invalid",
"binary": "3D 00 DEAD 0D0001000000 0E52",
"expected_error": "invalid_magic",
"notes": "Magic bytes are 0xDEAD instead of 0xC03E. Bridge receiver must reject frames with wrong magic."
},
{
"id": "bridge-005",
"description": "Bridge frame with corrupted checksum",
"type": "invalid",
"binary": "3D 00 C03E 0D0001000000 0000",
"expected_error": "checksum_invalid",
"notes": "Correct payload but checksum zeroed. Fletcher-16 verification must fail."
}
]
}
+94
View File
@@ -0,0 +1,94 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "Companion Protocol - Commands",
"spec_ref": "spec/18-companion-protocol.md",
"protocol_version": 0,
"description": "Tests companion protocol command type encoding for various command types.",
"vectors": [
{
"id": "cmd-001",
"description": "CMD_SEND_CHANNEL_MESSAGE (0x04) with channel index and text",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "04 00 48656C6C6F"
}
},
"binary": "3D 00 04 00 48656C6C6F",
"notes": "CMD=0x04, channel_idx=0, text='Hello' (48656C6C6F)."
},
{
"id": "cmd-002",
"description": "CMD_GET_CONTACTS (0x06)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "06"
}
},
"binary": "3D 00 06"
},
{
"id": "cmd-003",
"description": "CMD_GET_CHANNELS (0x07)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "07"
}
},
"binary": "3D 00 07"
},
{
"id": "cmd-004",
"description": "CMD_REBOOT (0x12)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "12"
}
},
"binary": "3D 00 12"
}
]
}
+74
View File
@@ -0,0 +1,74 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "Companion Protocol - Framing",
"spec_ref": "spec/18-companion-protocol.md",
"protocol_version": 0,
"description": "Tests companion protocol frame encoding: type byte followed by variable-length data.",
"vectors": [
{
"id": "comp-001",
"description": "CMD_APP_START (0x01) - no data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "01"
}
},
"binary": "3D 00 01",
"notes": "Companion command type 0x01 = CMD_APP_START. No payload data."
},
{
"id": "comp-002",
"description": "CMD_DEVICE_QUERY (0x02) - no data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "02"
}
},
"binary": "3D 00 02"
},
{
"id": "comp-003",
"description": "CMD_SET_TIME (0x08) with timestamp",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "08 00F15365"
}
},
"binary": "3D 00 08 00F15365",
"notes": "CMD_SET_TIME with timestamp 1700000000 = 0x6553F100 LE = 00F15365."
}
]
}
+53
View File
@@ -0,0 +1,53 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "Companion Protocol - Responses",
"spec_ref": "spec/18-companion-protocol.md",
"protocol_version": 0,
"description": "Tests companion protocol response type encoding.",
"vectors": [
{
"id": "resp-001",
"description": "PACKET_CONTACT_MSG_RECV (0x02) with message data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "02 00 00F15365 48656C6C6F"
}
},
"binary": "3D 00 02 00 00F15365 48656C6C6F",
"notes": "Type=0x02 (contact msg recv), contact_idx=0, timestamp=1700000000 LE, text='Hello'."
},
{
"id": "resp-002",
"description": "PACKET_ADV_RECV (0x03) with advert data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "03 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 01"
}
},
"binary": "3D 00 03 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 01",
"notes": "Type=0x03 (advert recv). pub_key=AA×32, flags=0x01 (Chat)."
}
]
}
+113
View File
@@ -0,0 +1,113 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "AES-128-ECB Block Padding",
"spec_ref": "spec/14-crypto.md",
"protocol_version": 0,
"description": "Tests AES-128-ECB zero-padding for partial blocks. MeshCore pads the final block with zeros before encryption.",
"vectors": [
{
"id": "aes-pad-001",
"description": "5-byte plaintext 'Hello' padded to 16 bytes with zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "3D 00 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "48656C6C6F"
},
"notes": "Padded input: 48656C6C6F 0000000000000000000000. Output: 1 block."
},
{
"id": "aes-pad-002",
"description": "15-byte plaintext padded with 1 zero byte",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "BF70C038D511F2A8B528E06691C6D42D"
}
},
"binary": "3D 00 BF70C038D511F2A8B528E06691C6D42D",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E"
},
"notes": "15 bytes padded with 1 zero: 000102...0E00. Output: 1 block."
},
{
"id": "aes-pad-003",
"description": "16-byte plaintext: no padding needed",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "0A940BB5416EF045F1C39458C653EA5A"
}
},
"binary": "3D 00 0A940BB5416EF045F1C39458C653EA5A",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F"
},
"notes": "Exact block boundary: no padding. Output: 1 block."
},
{
"id": "aes-pad-004",
"description": "17-byte plaintext: 2 blocks, second padded with 15 zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560"
}
},
"binary": "3D 00 0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F10"
},
"notes": "Block 1: 000102...0F (no pad). Block 2: 10 + 15 zeros. Output: 2 blocks = 32 bytes."
}
]
}
+110
View File
@@ -0,0 +1,110 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "AES-128-ECB Encryption",
"spec_ref": "spec/14-crypto.md",
"protocol_version": 0,
"description": "Standalone AES-128-ECB encryption test vectors. Key is the first 16 bytes of the shared secret. These vectors test the raw encryption operation without MAC.",
"vectors": [
{
"id": "aes-001",
"description": "AES-128-ECB: key=00..0F, plaintext=all zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C6A13B37878F5B826F4F8162A1C8D879"
}
},
"binary": "3D 00 C6A13B37878F5B826F4F8162A1C8D879",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "00000000000000000000000000000000"
},
"notes": "AES-128-ECB(key=00..0F, pt=00×16) = C6A13B37878F5B826F4F8162A1C8D879. Wrapped in RAW_CUSTOM for schema compliance."
},
{
"id": "aes-002",
"description": "AES-128-ECB: key=00..0F, plaintext=all FF",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "3C441F32CE07822364D7A2990E50BB13"
}
},
"binary": "3D 00 3C441F32CE07822364D7A2990E50BB13",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
}
},
{
"id": "aes-003",
"description": "AES-128-ECB: key=00..0F, plaintext=00..0F (sequential)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "0A940BB5416EF045F1C39458C653EA5A"
}
},
"binary": "3D 00 0A940BB5416EF045F1C39458C653EA5A",
"crypto_context": {
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F"
}
},
{
"id": "aes-004",
"description": "AES-128-ECB: key=all FF, plaintext=all zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "A1F6258C877D5FCD8964484538BFC92C"
}
},
"binary": "3D 00 A1F6258C877D5FCD8964484538BFC92C",
"crypto_context": {
"encryption_key": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"plaintext": "00000000000000000000000000000000"
}
}
]
}
+87
View File
@@ -0,0 +1,87 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "ECDH Shared Secret (X25519)",
"spec_ref": "spec/15-identity.md",
"protocol_version": 0,
"description": "X25519 ECDH key exchange test vectors. MeshCore uses Ed25519 keys transposed to X25519 for key exchange. The resulting 32-byte shared secret is used as: AES key (bytes 0-15) and HMAC key (bytes 0-31).",
"vectors": [
{
"id": "ecdh-001",
"description": "RFC 7748 Test Vector: X25519 key exchange (Alice's perspective)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "4A5D9D5BA4CE2DE1728E3BF480350F25E07E21C947D19E3376F09B3C1E161742"
}
},
"binary": "3D 00 4A5D9D5BA4CE2DE1728E3BF480350F25E07E21C947D19E3376F09B3C1E161742",
"crypto_context": {
"sender_private_key": "77076D0A7318A57D3C16C17251B26645DF4C2F87EBC0992AB177FBA51DB92C2A",
"recipient_public_key": "DE9EDB7D7B7DC1B4D35B61C2ECE435373F8343C85B78674DADFC7E146F882B4F"
},
"notes": "RFC 7748 Section 6.1. Alice's private key and Bob's public key produce the shared secret. Note: MeshCore transposes Ed25519 keys to X25519, so actual ECDH inputs differ from raw Ed25519 keys."
},
{
"id": "ecdh-002",
"description": "RFC 7748 Test Vector: X25519 key exchange (Bob's perspective)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "4A5D9D5BA4CE2DE1728E3BF480350F25E07E21C947D19E3376F09B3C1E161742"
}
},
"binary": "3D 00 4A5D9D5BA4CE2DE1728E3BF480350F25E07E21C947D19E3376F09B3C1E161742",
"crypto_context": {
"sender_private_key": "5DAB087E624A8A4B79E17F8B83800EE66F3BB1292618B6FD1C2F8B27FF88E0EB",
"recipient_public_key": "8520F0098930A754748B7DDCB43EF75A0DBF3A0D26381AF4EBA4A98EAA9B4E6A"
},
"notes": "RFC 7748 Section 6.1. Bob's private key and Alice's public key produce the same shared secret as ecdh-001, verifying the commutative property of ECDH."
},
{
"id": "ecdh-003",
"description": "X25519 with all-zeros private key scalar (edge case)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "0000000000000000000000000000000000000000000000000000000000000000"
}
},
"binary": "3D 00 0000000000000000000000000000000000000000000000000000000000000000",
"crypto_context": {
"sender_private_key": "0000000000000000000000000000000000000000000000000000000000000000",
"recipient_public_key": "DE9EDB7D7B7DC1B4D35B61C2ECE435373F8343C85B78674DADFC7E146F882B4F"
},
"notes": "Edge case: all-zero private key after clamping becomes a low-order scalar. X25519 with a clamped all-zero key (which becomes 0x40...00) against any public key. The all-zero shared secret indicates this is a degenerate key and implementations SHOULD reject it."
}
]
}
+90
View File
@@ -0,0 +1,90 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Ed25519 Sign/Verify",
"spec_ref": "spec/15-identity.md",
"protocol_version": 0,
"description": "Ed25519 signature test vectors. MeshCore uses Ed25519 for advertisement signing. These vectors use RFC 8032 test keypairs for cross-validation.",
"vectors": [
{
"id": "ed-001",
"description": "RFC 8032 Test Vector 1: empty message",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "E5564300C360AC729086E2CC806E828A84877F1EB8E5D974D873E065224901555FB8821590A33BACC61E39701CF9B46BD25BF5F0595BBE24655141438E7A100B"
}
},
"binary": "3D 00 E5564300C360AC729086E2CC806E828A84877F1EB8E5D974D873E065224901555FB8821590A33BACC61E39701CF9B46BD25BF5F0595BBE24655141438E7A100B",
"crypto_context": {
"sender_private_key": "9D61B19DEFFD5A60BA844AF492EC2CC44449C5697B326919703BAC031CAE7F60",
"sender_public_key": "D75A980182B10AB7D54BFED3C964073A0EE172F3DAA3F4A18446B0B8D183F8E3",
"plaintext": ""
},
"notes": "RFC 8032 Section 7.1 Test Vector 1. Private key (seed) is 32 bytes; the 64-byte format used by MeshCore includes the expanded key. Signature over empty message."
},
{
"id": "ed-002",
"description": "RFC 8032 Test Vector 2: single-byte message 0x72",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "92A009A9F0D4CAB8720E820B5F642540A2B27B5416503F8FB3762223EBDB69DA085AC1E43E159C7E94B2BA35F6F6A1918E25682F9B0ADC5C33C2BF3C39F04E50"
}
},
"binary": "3D 00 92A009A9F0D4CAB8720E820B5F642540A2B27B5416503F8FB3762223EBDB69DA085AC1E43E159C7E94B2BA35F6F6A1918E25682F9B0ADC5C33C2BF3C39F04E50",
"crypto_context": {
"sender_private_key": "4CCD089B28FF96DA9DB6C346EC114E0F5B8A319F35ABA624DA8CF6ED4FB8A6FB",
"sender_public_key": "3D4017C3E843895A92B70AA74D1B7EBC9C982CCF2EC4968CC0CD55F12AF4660C",
"plaintext": "72"
},
"notes": "RFC 8032 Section 7.1 Test Vector 2. Signature over the single byte 0x72."
},
{
"id": "ed-003",
"description": "RFC 8032 Test Vector 3: two-byte message",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "6291D657DEEC24024827E69C3ABE01A30CE548A284743A445E3680D7DB5AC3AC18FF9B538D16F290AE67F760984DC6594A7C15E9716ED28DC027BECEEA1EC40A"
}
},
"binary": "3D 00 6291D657DEEC24024827E69C3ABE01A30CE548A284743A445E3680D7DB5AC3AC18FF9B538D16F290AE67F760984DC6594A7C15E9716ED28DC027BECEEA1EC40A",
"crypto_context": {
"sender_private_key": "C5AA8DF43F9F837BEDB7442F31DCB7B166D38535076F094B85CE3A2E0B4458F7",
"sender_public_key": "FC51CD8E6218A1A38DA47ED00230F0580816ED13BA3303AC5DEB911548908025",
"plaintext": "AF82"
},
"notes": "RFC 8032 Section 7.1 Test Vector 3. Signature over the two-byte message 0xAF82."
}
]
}
@@ -0,0 +1,63 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypt-then-MAC Roundtrip",
"spec_ref": "spec/14-crypto.md",
"protocol_version": 0,
"description": "Full encrypt-then-MAC pipeline test vectors. Each vector provides the shared secret, plaintext, and expected MAC+ciphertext output. Implementations should verify: (1) encrypt produces the ciphertext, (2) HMAC produces the MAC, (3) MACThenDecrypt recovers the plaintext.",
"vectors": [
{
"id": "etm-001",
"description": "Encrypt-then-MAC: 'Hello' with key 00..1F",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "EAB5 D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "3D 00 EAB5D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "48656C6C6F"
},
"notes": "AES key = secret[0..15], HMAC key = secret[0..31]. Output = MAC(2) || Ciphertext(16) = 18 bytes."
},
{
"id": "etm-002",
"description": "Encrypt-then-MAC: 'Pipeline test!' with key 00..1F",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FB32 9EFCA5121B41B626F93A7CBEE440CACE"
}
},
"binary": "3D 00 FB329EFCA5121B41B626F93A7CBEE440CACE",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "506970656C696E65207465737421"
},
"notes": "Plaintext 'Pipeline test!' (14 bytes). Zero-padded to 16 bytes before AES encryption."
}
]
}
@@ -0,0 +1,61 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "HMAC-SHA256 Truncated to 2 Bytes",
"spec_ref": "spec/14-crypto.md",
"protocol_version": 0,
"description": "Tests HMAC-SHA256 computation with 32-byte key, truncated to CIPHER_MAC_SIZE (2 bytes). The HMAC key is the full 32-byte shared secret (PUB_KEY_SIZE), not just the 16-byte AES key.",
"vectors": [
{
"id": "hmac-001",
"description": "HMAC-SHA256(key=00..1F, msg='test message') truncated to 2 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "90B8"
}
},
"binary": "3D 00 90B8",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "74657374206D657373616765"
},
"notes": "HMAC-SHA256(key=00..1F, 'test message') = 90B89BC3C5CA2C29... Truncated to 2 bytes = 90B8."
},
{
"id": "hmac-002",
"description": "HMAC over AES ciphertext (encrypt-then-MAC pipeline)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "EAB5"
}
},
"binary": "3D 00 EAB5",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "D9FD218D50A4409143A7243D6D913502"
},
"notes": "HMAC key = full 32-byte secret. Message = the AES ciphertext of 'Hello'. Result truncated to 2 bytes = EAB5."
}
]
}
+84
View File
@@ -0,0 +1,84 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "SHA-256 Basic",
"spec_ref": "spec/14-crypto.md",
"protocol_version": 0,
"description": "Basic SHA-256 hash computation test vectors. MeshCore uses SHA-256 for packet hashing, channel hash derivation, and HMAC.",
"vectors": [
{
"id": "sha-001",
"description": "SHA-256 of empty string",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
}
},
"binary": "3D 00 E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"crypto_context": {
"plaintext": ""
},
"notes": "SHA-256('') = E3B0C442...B855. Standard NIST test vector."
},
{
"id": "sha-002",
"description": "SHA-256 of 'abc'",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD"
}
},
"binary": "3D 00 BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
"crypto_context": {
"plaintext": "616263"
},
"notes": "SHA-256('abc') = BA7816BF...15AD. Standard NIST test vector."
},
{
"id": "sha-003",
"description": "SHA-256 of 'MeshCore'",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "F4F17278283E082A4743CCA308285D601E0F9943E6D6F9271B5B8C7DFED09DC0"
}
},
"binary": "3D 00 F4F17278283E082A4743CCA308285D601E0F9943E6D6F9271B5B8C7DFED09DC0",
"crypto_context": {
"plaintext": "4D657368436F7265"
},
"notes": "SHA-256('MeshCore')."
}
]
}
+109
View File
@@ -0,0 +1,109 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "SHA-256 Packet Hash",
"spec_ref": "spec/16-packet-hash.md",
"protocol_version": 0,
"description": "Tests MeshCore packet hash computation: SHA-256(payload_type || payload)[0..7]. TRACE packets additionally include path_len in the hash input.",
"vectors": [
{
"id": "phash-001",
"description": "Packet hash for ACK with CRC 0xDEADBEEF",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "1BEE08540E8F7E5B"
}
},
"binary": "3D 00 1BEE08540E8F7E5B",
"crypto_context": {
"plaintext": "03 EFBEADDE"
},
"notes": "SHA-256(0x03 || EFBEADDE)[0..7] = 1BEE08540E8F7E5B. Input: type byte 0x03 (ACK) + CRC 0xDEADBEEF as LE."
},
{
"id": "phash-002",
"description": "Packet hash for TRACE (path_len=0): includes path_len in hash",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C105C34E45E60009"
}
},
"binary": "3D 00 C105C34E45E60009",
"crypto_context": {
"plaintext": "09 00 01000000 02000000 00"
},
"notes": "SHA-256(0x09 || 0x00 || tag=1_LE || auth=2_LE || flags=0)[0..7]. TRACE special case: path_len byte (0x00) included after type byte."
},
{
"id": "phash-003",
"description": "Packet hash for TRACE (path_len=3): different hash due to path_len",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "B83FB2E0EE276404"
}
},
"binary": "3D 00 B83FB2E0EE276404",
"crypto_context": {
"plaintext": "09 03 01000000 02000000 00"
},
"notes": "Same TRACE payload but path_len=3 instead of 0. Different hash proves path_len is included in computation."
},
{
"id": "phash-004",
"description": "Packet hash for ADVERT (all-AA pubkey, ts=0, all-BB sig)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "F73157720FB1B5E1"
}
},
"binary": "3D 00 F73157720FB1B5E1",
"crypto_context": {
"plaintext": "04 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
},
"notes": "SHA-256(0x04 || advert_payload)[0..7]. Type 0x04 = ADVERT. Not a TRACE, so path_len is NOT included."
}
]
}
+141
View File
@@ -0,0 +1,141 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "KISS Protocol - Framing",
"spec_ref": "spec/19-kiss-protocol.md",
"protocol_version": 0,
"description": "Tests KISS frame encoding including FEND delimiters, byte stuffing (FESC sequences), and type byte encoding.",
"vectors": [
{
"id": "kiss-001",
"description": "Simple data frame: FEND + type(0x00) + data + FEND",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 00 DEADBEEF C0"
}
},
"binary": "3D 00 C0 00 DEADBEEF C0",
"notes": "KISS frame: FEND(C0) + DataFrame(00) + 4 bytes data + FEND(C0). Data contains no special bytes so no escaping needed."
},
{
"id": "kiss-002",
"description": "Data frame with FEND in data (byte stuffing required)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 00 AA DBDC BB C0"
}
},
"binary": "3D 00 C0 00 AA DBDC BB C0",
"notes": "Data byte 0xC0 escaped as DB DC (FESC TFEND). Original data: AA C0 BB."
},
{
"id": "kiss-003",
"description": "Data frame with FESC in data (byte stuffing required)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 00 AA DBDD BB C0"
}
},
"binary": "3D 00 C0 00 AA DBDD BB C0",
"notes": "Data byte 0xDB escaped as DB DD (FESC TFESC). Original data: AA DB BB."
},
{
"id": "kiss-004",
"description": "Data frame with both FEND and FESC in data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 00 DBDC DBDD C0"
}
},
"binary": "3D 00 C0 00 DBDC DBDD C0",
"notes": "Original data: C0 DB. Escaped: DBDC DBDD."
},
{
"id": "kiss-005",
"description": "SetHardware frame: GetIdentity (sub-cmd 0x01)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 01 C0"
}
},
"binary": "3D 00 C0 06 01 C0",
"notes": "KISS frame: FEND + SetHardware(06) + GetIdentity(01) + FEND."
},
{
"id": "kiss-006",
"description": "SetHardware response: Identity (sub-cmd 0x81) with pubkey",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 81 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA C0"
}
},
"binary": "3D 00 C0 06 81 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA C0",
"notes": "KISS response: FEND + SetHardware(06) + Identity(81) + 32-byte pubkey + FEND."
}
]
}
+185
View File
@@ -0,0 +1,185 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "KISS Protocol - SetHardware Extensions",
"spec_ref": "spec/19-kiss-protocol.md",
"protocol_version": 0,
"description": "Tests MeshCore-specific SetHardware (0x06) sub-commands and responses.",
"vectors": [
{
"id": "kiss-hw-001",
"description": "GetVersion request",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 11 C0"
}
},
"binary": "3D 00 C0 06 11 C0",
"notes": "SetHardware(06) + GetVersion(11)."
},
{
"id": "kiss-hw-002",
"description": "Version response: version=1, reserved=0",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 91 01 00 C0"
}
},
"binary": "3D 00 C0 06 91 01 00 C0",
"notes": "SetHardware(06) + Version response(91) + version=1 + reserved=0."
},
{
"id": "kiss-hw-003",
"description": "SetRadio: freq=869618000Hz, bw=62500Hz, sf=12, cr=5",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 09 D0C5D733 A4F40000 0C 05 C0"
}
},
"binary": "3D 00 C0 06 09 D0C5D733 A4F40000 0C 05 C0",
"notes": "SetRadio(09). freq=869618000=0x33D7C5D0 LE=D0C5D733. bw=62500=0x0000F4A4 LE=A4F40000. sf=12, cr=5."
},
{
"id": "kiss-hw-004",
"description": "Ping request and Pong response",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 17 C0"
}
},
"binary": "3D 00 C0 06 17 C0",
"notes": "SetHardware(06) + Ping(17)."
},
{
"id": "kiss-hw-005",
"description": "Error response: UnknownCmd (0x05)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 F1 05 C0"
}
},
"binary": "3D 00 C0 06 F1 05 C0",
"notes": "SetHardware(06) + Error(F1) + UnknownCmd(05)."
},
{
"id": "kiss-hw-006",
"description": "RxMeta unsolicited event: SNR=-2.5dB, RSSI=-100dBm",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 F9 F6 9C C0"
}
},
"binary": "3D 00 C0 06 F9 F6 9C C0",
"notes": "RxMeta(F9). SNR: -2.5dB × 4 = -10 = 0xF6 (signed byte). RSSI: -100dBm = 0x9C (signed byte)."
},
{
"id": "kiss-hw-007",
"description": "TxDone success",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 F8 01 C0"
}
},
"binary": "3D 00 C0 06 F8 01 C0",
"notes": "TxDone(F8) + success(01)."
},
{
"id": "kiss-hw-008",
"description": "Stats response: rx=100, tx=50, errors=2",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 06 92 64000000 32000000 02000000 C0"
}
},
"binary": "3D 00 C0 06 92 64000000 32000000 02000000 C0",
"notes": "Stats(92). rx=100=0x64 LE=64000000, tx=50=0x32 LE=32000000, errors=2 LE=02000000."
}
]
}
+97
View File
@@ -0,0 +1,97 @@
{
"$schema": "../schema/test-vector.schema.json",
"name": "KISS Protocol - Standard Commands",
"spec_ref": "spec/19-kiss-protocol.md",
"protocol_version": 0,
"description": "Tests standard KISS commands (TXDELAY, Persistence, SlotTime, etc.).",
"vectors": [
{
"id": "kiss-std-001",
"description": "TXDELAY command: 500ms (50 × 10ms)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 01 32 C0"
}
},
"binary": "3D 00 C0 01 32 C0",
"notes": "Type=0x01 (TXDELAY), data=0x32 (50 decimal = 500ms)."
},
{
"id": "kiss-std-002",
"description": "Persistence command: P=63",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 02 3F C0"
}
},
"binary": "3D 00 C0 02 3F C0",
"notes": "Type=0x02 (Persistence), data=0x3F (63)."
},
{
"id": "kiss-std-003",
"description": "SlotTime command: 100ms (10 × 10ms)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 03 0A C0"
}
},
"binary": "3D 00 C0 03 0A C0",
"notes": "Type=0x03 (SlotTime), data=0x0A (10 = 100ms)."
},
{
"id": "kiss-std-004",
"description": "FullDuplex command: half duplex (0)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "C0 05 00 C0"
}
},
"binary": "3D 00 C0 05 00 C0",
"notes": "Type=0x05 (FullDuplex), data=0x00 (half duplex)."
}
]
}
+122
View File
@@ -0,0 +1,122 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "ACK Payload - Basic",
"spec_ref": "spec/04-payload-ack.md",
"protocol_version": 0,
"description": "Basic ACK payload encoding/decoding tests with various CRC values.",
"vectors": [
{
"id": "ack-001",
"description": "ACK with CRC 0xDEADBEEF",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "DEADBEEF"
}
},
"binary": "0D 00 EFBEADDE",
"notes": "CRC 0xDEADBEEF stored as little-endian: EF BE AD DE."
},
{
"id": "ack-002",
"description": "ACK with CRC 0x00000001",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 00 01000000"
},
{
"id": "ack-003",
"description": "ACK with CRC 0x12345678",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "12345678"
}
},
"binary": "0D 00 78563412"
},
{
"id": "ack-004",
"description": "ACK via direct routing with path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 2,
"hashes": [
"AA",
"BB"
]
},
"payload": {
"ack_crc": "CAFEBABE"
}
},
"binary": "0E 02 AABB BEBAFECA",
"notes": "Direct routed ACK. CRC 0xCAFEBABE as LE = BEBAFECA."
},
{
"id": "ack-005",
"description": "ACK with 2-byte hash path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "direct"
},
"path": {
"hash_size": 2,
"hash_count": 1,
"hashes": [
"AABB"
]
},
"payload": {
"ack_crc": "00000000"
}
},
"binary": "0E 41 AABB 00000000",
"notes": "path_len=0x41: hash_size=2, count=1."
}
]
}
+115
View File
@@ -0,0 +1,115 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "ACK Payload - Boundary Values",
"spec_ref": "spec/04-payload-ack.md",
"protocol_version": 0,
"description": "ACK CRC boundary value tests including all-zeros, all-ones, and single-bit patterns.",
"vectors": [
{
"id": "ack-bnd-001",
"description": "ACK CRC all zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000000"
}
},
"binary": "0D 00 00000000"
},
{
"id": "ack-bnd-002",
"description": "ACK CRC all ones",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "FFFFFFFF"
}
},
"binary": "0D 00 FFFFFFFF"
},
{
"id": "ack-bnd-003",
"description": "ACK CRC 0x80000000 (high bit set)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "80000000"
}
},
"binary": "0D 00 00000080"
},
{
"id": "ack-bnd-004",
"description": "ACK CRC 0x00000080 (byte boundary)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000080"
}
},
"binary": "0D 00 80000000"
},
{
"id": "ack-bnd-005",
"description": "ACK CRC 0x01000000",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "01000000"
}
},
"binary": "0D 00 00000001",
"notes": "Tests that 0x01000000 LE = 00 00 00 01, not confused with 0x00000001."
}
]
}
@@ -0,0 +1,128 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Advertisement App Data - Combined Optional Fields",
"spec_ref": "spec/05-payload-advert.md",
"protocol_version": 0,
"description": "Tests app_data with multiple optional fields present simultaneously. Fields appear in order: flags, location (lat+lon), feat1, feat2, name.",
"vectors": [
{
"id": "adc-001",
"description": "Chat node with location + feat1 + name",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 177,
"latitude": 37774900,
"longitude": -122419400,
"feat1": 256,
"name": "Node1"
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB B1 34664002 3807B4F8 0001 4E6F646531",
"notes": "flags=0xB1=10110001: type=Chat(1), has_location(bit4), has_feat1(bit5), has_name(bit7). No feat2 (bit6=0). feat1=256=0x0100 LE=0001. Name 'Node1' = 4E6F646531."
},
{
"id": "adc-002",
"description": "Repeater with all optional fields",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 242,
"latitude": 0,
"longitude": 0,
"feat1": 1,
"feat2": 2,
"name": "RPT"
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB F2 00000000 00000000 0100 0200 525054",
"notes": "flags=0xF2=11110010: type=Repeater(2), all optional fields present. feat1=1 LE=0100, feat2=2 LE=0200, name='RPT'=525054."
},
{
"id": "adc-003",
"description": "Node with name only (no location or features)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 129,
"name": "Hi"
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 81 4869",
"notes": "flags=0x81=10000001: type=Chat(1), has_name(bit7) only. name='Hi'=4869."
},
{
"id": "adc-004",
"description": "Node with feat2 only (no location, feat1, or name)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 65,
"feat2": 65535
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 41 FFFF",
"notes": "flags=0x41=01000001: type=Chat(1), has_feat2(bit6). feat2=65535=0xFFFF."
}
]
}
@@ -0,0 +1,96 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Advertisement App Data - Location",
"spec_ref": "spec/05-payload-advert.md",
"protocol_version": 0,
"description": "Tests app_data with location fields (latitude and longitude as int32_le × 1,000,000).",
"vectors": [
{
"id": "adl-001",
"description": "Chat node with location: San Francisco (37.7749, -122.4194)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 17,
"latitude": 37774900,
"longitude": -122419400
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 11 34664002 3807B4F8",
"notes": "flags=0x11: type=Chat(1), has_location(bit 4). Lat 37774900=0x02406634 LE=34664002. Lon -122419400=0xF8B40738 LE=3807B4F8."
},
{
"id": "adl-002",
"description": "Repeater with location: equator/prime meridian (0, 0)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 18,
"latitude": 0,
"longitude": 0
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 12 00000000 00000000",
"notes": "flags=0x12: type=Repeater(2), has_location. Both coords zero."
},
{
"id": "adl-003",
"description": "Sensor with negative latitude: south pole (-90, 0)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 20,
"latitude": -90000000,
"longitude": 0
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 14 80B5A2FA 00000000",
"notes": "flags=0x14: type=Sensor(4), has_location. Lat -90000000=0xFAA2B580 LE=80B5A2FA. Lon 0."
}
]
}
+140
View File
@@ -0,0 +1,140 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Advertisement App Data - Node Types",
"spec_ref": "spec/05-payload-advert.md",
"protocol_version": 0,
"description": "Tests all defined node types in the app_data flags field (bits 0-3). Uses a common pub_key, zero timestamp, and zero signature for structural testing.",
"vectors": [
{
"id": "adt-001",
"description": "App data type: None (0)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 0
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 00",
"notes": "flags=0x00: type=None(0), no optional fields."
},
{
"id": "adt-002",
"description": "App data type: Chat (1)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 1
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 01"
},
{
"id": "adt-003",
"description": "App data type: Repeater (2)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 2
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 02"
},
{
"id": "adt-004",
"description": "App data type: Room (3)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 3
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 03"
},
{
"id": "adt-005",
"description": "App data type: Sensor (4)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 0,
"signature": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"app_data": {
"flags": 4
}
}
},
"binary": "11 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00000000 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 04"
}
]
}
+88
View File
@@ -0,0 +1,88 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Advertisement Payload - Basic Structure",
"spec_ref": "spec/05-payload-advert.md",
"protocol_version": 0,
"description": "Tests the basic structure of advertisement payloads: pub_key(32) + timestamp(4) + signature(64) + optional app_data. Uses synthetic signature bytes since signature verification requires real Ed25519 operations (see signature-verify.json for verifiable vectors).",
"vectors": [
{
"id": "adv-001",
"description": "Minimal advertisement: pub_key + timestamp + signature, no app_data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20",
"timestamp": 1700000000,
"signature": "A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0"
}
},
"binary": "11 00 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20 00F15365 A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0",
"notes": "Header: 0x11 = ADVERT+FLOOD. Timestamp 1700000000 = 0x6553F100 LE = 00F15365. Payload: 32+4+64 = 100 bytes."
},
{
"id": "adv-002",
"description": "Advertisement with 1-byte app_data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"pub_key": "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20",
"timestamp": 1700000000,
"signature": "A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0",
"app_data": {
"flags": 1
}
}
},
"binary": "11 00 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20 00F15365 A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0 01",
"notes": "flags=0x01: type=Chat(1), no optional fields. Payload: 101 bytes."
},
{
"id": "adv-003",
"description": "Advertisement with path (3 hops, direct routing)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 3,
"hashes": [
"AA",
"BB",
"CC"
]
},
"payload": {
"pub_key": "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20",
"timestamp": 0,
"signature": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
},
"binary": "11 03 AABBCC 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20 00000000 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"notes": "Timestamp=0 and zero signature for structural testing."
}
]
}
+85
View File
@@ -0,0 +1,85 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Anonymous Request Payload",
"spec_ref": "spec/07-payload-anon-req.md",
"protocol_version": 0,
"description": "Tests anonymous request payload: dest_hash(1) + sender_pub_key(32) + cipher_mac(2) + ciphertext.",
"vectors": [
{
"id": "anon-001",
"description": "Anonymous request with known keys",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "anon_req",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"sender_pub_key": "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F",
"cipher_mac": "F4B4",
"ciphertext": "908A4FC137435774D0BB603F55A06069"
}
},
"binary": "1D 00 AB 404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F F4B4 908A4FC137435774D0BB603F55A06069",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "416E6F6E526571"
},
"notes": "Header 0x1D = ANON_REQ(7)+FLOOD(1). Payload: dest_hash(1) + pub_key(32) + mac(2) + ct(16) = 51 bytes. Plaintext 'AnonReq' (7 bytes)."
},
{
"id": "anon-002",
"description": "Anonymous request with tampered MAC",
"type": "invalid",
"binary": "1D 00 AB 404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F 0000 908A4FC137435774D0BB603F55A06069",
"expected_error": "mac_invalid",
"notes": "MAC zeroed. Verification must fail."
},
{
"id": "anon-003",
"description": "Anonymous request via DIRECT routing with 2-hop path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "anon_req",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 2,
"hashes": ["AA", "BB"]
},
"payload": {
"dest_hash": "CC",
"sender_pub_key": "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F",
"cipher_mac": "F4B4",
"ciphertext": "908A4FC137435774D0BB603F55A06069"
}
},
"binary": "1E 02 AABB CC 404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F F4B4 908A4FC137435774D0BB603F55A06069",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "416E6F6E526571"
},
"notes": "Header 0x1E = ANON_REQ(7)+DIRECT(2). Direct-routed anonymous request with 2 path hashes."
},
{
"id": "anon-004",
"description": "Anonymous request truncated before ciphertext",
"type": "invalid",
"binary": "1D 00 AB 404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F F4B4",
"expected_error": "too_short",
"notes": "Payload is only 35 bytes (dest_hash + pub_key + mac). Missing ciphertext. Minimum is 51 bytes."
}
]
}
+96
View File
@@ -0,0 +1,96 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Control Payload - Basic",
"spec_ref": "spec/12-payload-control.md",
"protocol_version": 0,
"description": "Tests control payload encoding. Control packets carry raw bytes where the first byte's bit 7 indicates zero-hop-only restriction.",
"vectors": [
{
"id": "ctl-001",
"description": "Control packet with multi-hop allowed (bit 7 clear)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "control",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "01 AABBCCDD"
}
},
"binary": "2D 00 01 AABBCCDD",
"notes": "control_byte=0x01: bit 7 clear, multi-hop allowed."
},
{
"id": "ctl-002",
"description": "Control packet with zero-hop only (bit 7 set)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "control",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "80 AABBCCDD"
}
},
"binary": "2E 00 80 AABBCCDD",
"notes": "control_byte=0x80: bit 7 set, zero-hop only. Direct route with path_count=0 = zero-hop."
},
{
"id": "ctl-003",
"description": "Minimal control packet (1 byte)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "control",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "00"
}
},
"binary": "2D 00 00"
},
{
"id": "ctl-004",
"description": "Control packet with zero-hop restriction and data via direct routing",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "control",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF 0102030405"
}
},
"binary": "2E 00 FF 0102030405",
"notes": "control_byte=0xFF: bit 7 set (zero-hop), all other bits set."
}
]
}
+129
View File
@@ -0,0 +1,129 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypted Payload - Basic",
"spec_ref": "spec/06-payload-encrypted.md",
"protocol_version": 0,
"description": "Tests encrypted payload encoding (REQ/RESPONSE/TXT_MSG) with known cryptographic material. Uses shared_secret = 0x000102...1F, AES key = first 16 bytes, HMAC key = full 32 bytes.",
"vectors": [
{
"id": "enc-001",
"description": "Encrypted TXT_MSG with 5-byte plaintext 'Hello'",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "09 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "48656C6C6F"
},
"notes": "plaintext 'Hello' = 48656C6C6F (5 bytes), zero-padded to 16 for AES-128-ECB. MAC is HMAC-SHA256(shared_secret, ciphertext) truncated to 2 bytes."
},
{
"id": "enc-002",
"description": "Encrypted REQUEST with 16-byte plaintext (exact AES block)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "request",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "631C",
"ciphertext": "0A940BB5416EF045F1C39458C653EA5A"
}
},
"binary": "01 00 ABCD 631C 0A940BB5416EF045F1C39458C653EA5A",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F"
},
"notes": "16-byte plaintext fills exactly one AES block. No padding needed."
},
{
"id": "enc-003",
"description": "Encrypted RESPONSE with 17-byte plaintext (spans 2 AES blocks)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "response",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "9822",
"ciphertext": "0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560"
}
},
"binary": "05 00 ABCD 9822 0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F10"
},
"notes": "17-byte plaintext: first 16 bytes in block 1, byte 17 zero-padded in block 2. 32 bytes ciphertext total."
},
{
"id": "enc-004",
"description": "Encrypted TXT_MSG with 1-byte plaintext",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "83FB",
"ciphertext": "2D4135E80761429386BFE3833C535AA8"
}
},
"binary": "09 00 ABCD 83FB 2D4135E80761429386BFE3833C535AA8",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "42"
},
"notes": "1-byte plaintext 0x42, zero-padded to 16 bytes."
}
]
}
+69
View File
@@ -0,0 +1,69 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypted Payload - MAC Verification",
"spec_ref": "spec/06-payload-encrypted.md",
"protocol_version": 0,
"description": "Tests MAC verification: valid MACs must pass, tampered MACs or ciphertext must fail.",
"vectors": [
{
"id": "mac-001",
"description": "Valid MAC with known ciphertext",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "09 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
}
},
{
"id": "mac-002",
"description": "Tampered MAC (first byte changed)",
"type": "invalid",
"binary": "09 00 ABCD 00B5 D9FD218D50A4409143A7243D6D913502",
"expected_error": "mac_invalid",
"notes": "MAC changed from EAB5 to 00B5. MACThenDecrypt must return 0."
},
{
"id": "mac-003",
"description": "Tampered MAC (second byte changed)",
"type": "invalid",
"binary": "09 00 ABCD EA00 D9FD218D50A4409143A7243D6D913502",
"expected_error": "mac_invalid",
"notes": "MAC changed from EAB5 to EA00."
},
{
"id": "mac-004",
"description": "Tampered ciphertext (first byte changed)",
"type": "invalid",
"binary": "09 00 ABCD EAB5 00FD218D50A4409143A7243D6D913502",
"expected_error": "mac_invalid",
"notes": "Ciphertext first byte changed from D9 to 00. HMAC over modified ciphertext won't match."
},
{
"id": "mac-005",
"description": "Swapped MAC bytes",
"type": "invalid",
"binary": "09 00 ABCD B5EA D9FD218D50A4409143A7243D6D913502",
"expected_error": "mac_invalid",
"notes": "MAC bytes reversed: B5EA instead of EAB5."
}
]
}
+99
View File
@@ -0,0 +1,99 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypted Payload - AES Block Padding",
"spec_ref": "spec/06-payload-encrypted.md",
"protocol_version": 0,
"description": "Tests AES-128-ECB zero-padding behavior at block boundaries. Plaintexts of 1, 15, 16, 17, and 32 bytes exercise all padding edge cases.",
"vectors": [
{
"id": "pad-001",
"description": "1-byte plaintext: padded with 15 zero bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "83FB",
"ciphertext": "2D4135E80761429386BFE3833C535AA8"
}
},
"binary": "09 00 ABCD 83FB 2D4135E80761429386BFE3833C535AA8",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "42"
},
"notes": "1 byte padded to 16: 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00. 1 AES block output."
},
{
"id": "pad-002",
"description": "16-byte plaintext: no padding needed (exact block boundary)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "631C",
"ciphertext": "0A940BB5416EF045F1C39458C653EA5A"
}
},
"binary": "09 00 ABCD 631C 0A940BB5416EF045F1C39458C653EA5A",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F"
},
"notes": "Exactly 16 bytes: no padding. 1 AES block output."
},
{
"id": "pad-003",
"description": "17-byte plaintext: 1 byte in second block, padded with 15 zeros",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "9822",
"ciphertext": "0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560"
}
},
"binary": "09 00 ABCD 9822 0A940BB5416EF045F1C39458C653EA5A299F7C29A3E13AE7F64ECBA062FC7560",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "000102030405060708090A0B0C0D0E0F10"
},
"notes": "17 bytes spans 2 blocks. Block 2 = 10 00 00 ... 00. 32 bytes ciphertext."
}
]
}
+47
View File
@@ -0,0 +1,47 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypted Payload - Roundtrip Verification",
"spec_ref": "spec/06-payload-encrypted.md",
"protocol_version": 0,
"description": "Roundtrip encrypt-then-MAC verification vectors. Each vector provides full key material enabling implementations to verify: (1) encryption produces the given ciphertext, (2) MAC computation produces the given MAC, (3) MAC verification succeeds, (4) decryption recovers the plaintext.",
"vectors": [
{
"id": "rt-enc-001",
"description": "Roundtrip: encrypt 'Hello', verify MAC, decrypt back",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "09 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"encryption_key": "000102030405060708090A0B0C0D0E0F",
"plaintext": "48656C6C6F"
},
"notes": "Verification steps: (1) AES-128-ECB encrypt(key=00..0F, pad('Hello')) -> D9FD...3502. (2) HMAC-SHA256(key=00..1F, ct)[:2] -> EAB5. (3) HMAC verify: recompute and compare. (4) AES-128-ECB decrypt -> 48656C6C6F + 11 zero bytes."
},
{
"id": "rt-enc-002",
"description": "Roundtrip with wrong MAC must fail verification",
"type": "invalid",
"binary": "09 00 ABCD FFFF D9FD218D50A4409143A7243D6D913502",
"expected_error": "mac_invalid",
"notes": "Same ciphertext as rt-enc-001 but MAC replaced with FFFF. MACThenDecrypt must return 0 (failure)."
}
]
}
+76
View File
@@ -0,0 +1,76 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Encrypted Payload - Extra Edge Cases",
"spec_ref": "spec/06-payload-encrypted.md",
"protocol_version": 0,
"description": "Additional encrypted payload tests: non-FLOOD routes, hash_size>1 paths, 32-byte plaintext, and payload-level invalid cases.",
"vectors": [
{
"id": "enc-extra-001",
"description": "Encrypted TXT_MSG via DIRECT routing with 2-byte hash path",
"type": "encode_decode",
"structured": {
"header": { "version": 0, "payload_type": "txt_msg", "route_type": "direct" },
"path": { "hash_size": 2, "hash_count": 1, "hashes": ["AABB"] },
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "0A 41 AABB ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
},
"notes": "Header 0x0A = TXT_MSG(2)+DIRECT(2). path_len=0x41 (hash_size=2, count=1). Tests encrypted payload with non-FLOOD routing and multi-byte hash."
},
{
"id": "enc-extra-002",
"description": "Encrypted TXT_MSG via TRANSPORT_FLOOD with transport codes",
"type": "encode_decode",
"structured": {
"header": { "version": 0, "payload_type": "txt_msg", "route_type": "transport_flood" },
"transport_codes": [1000, 0],
"path": { "hash_size": 1, "hash_count": 0, "hashes": [] },
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "08 E803 0000 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
},
"notes": "Header 0x08 = TXT_MSG(2)+TRANSPORT_FLOOD(0). Tests encrypted payload with transport codes."
},
{
"id": "enc-extra-003",
"description": "ACK payload too short (3 bytes instead of 4)",
"type": "invalid",
"binary": "0D 00 EFBEAD",
"expected_error": "incomplete_payload",
"notes": "ACK requires exactly 4 bytes. Only 3 provided."
},
{
"id": "enc-extra-004",
"description": "Payload exceeds MAX_PACKET_PAYLOAD (185 bytes)",
"type": "invalid",
"binary": "3D 00 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8",
"expected_error": "payload_too_large",
"notes": "Payload is 185 bytes (0x00..0xB8), exceeding MAX_PACKET_PAYLOAD of 184. readFrom MUST reject."
},
{
"id": "enc-extra-005",
"description": "Header 0xFF sentinel on wire (must reject)",
"type": "invalid",
"binary": "FF 00 DEADBEEF",
"expected_error": "sentinel_header",
"notes": "Header 0xFF is used internally as do-not-retransmit marker. MUST NOT appear on the wire."
}
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Group Data Payload",
"spec_ref": "spec/08-payload-group.md",
"protocol_version": 0,
"description": "Tests group datagram payload. Same wire format as GRP_TXT but payload type = 0x06.",
"vectors": [
{
"id": "grp-data-001",
"description": "Group datagram with known channel secret",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "grp_data",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"channel_hash": "72",
"cipher_mac": "D184",
"ciphertext": "948C819389BA4ED7B1194A0FF8E62073"
}
},
"binary": "19 00 72 D184 948C819389BA4ED7B1194A0FF8E62073",
"crypto_context": {
"shared_secret": "202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F",
"encryption_key": "202122232425262728292A2B2C2D2E2F",
"plaintext": "47726F75704D736721"
},
"notes": "Header 0x19 = GRP_DATA(6)+FLOOD(1). Same ciphertext as GRP_TXT since same key and plaintext."
}
]
}
+46
View File
@@ -0,0 +1,46 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Group Text Payload",
"spec_ref": "spec/08-payload-group.md",
"protocol_version": 0,
"description": "Tests group text message payload: channel_hash(1) + cipher_mac(2) + ciphertext.",
"vectors": [
{
"id": "grp-txt-001",
"description": "Group text message with known channel secret",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "grp_txt",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"channel_hash": "72",
"cipher_mac": "D184",
"ciphertext": "948C819389BA4ED7B1194A0FF8E62073"
}
},
"binary": "15 00 72 D184 948C819389BA4ED7B1194A0FF8E62073",
"crypto_context": {
"shared_secret": "202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F",
"encryption_key": "202122232425262728292A2B2C2D2E2F",
"plaintext": "47726F75704D736721"
},
"notes": "Channel hash = SHA256(channel_secret)[0] = 0x72. Plaintext 'GroupMsg!' (9 bytes). AES key = first 16 bytes of channel secret."
},
{
"id": "grp-txt-002",
"description": "Group text with tampered MAC must fail",
"type": "invalid",
"binary": "15 00 72 0000 948C819389BA4ED7B1194A0FF8E62073",
"expected_error": "mac_invalid",
"notes": "Same ciphertext as grp-txt-001 but MAC zeroed. Verification must fail."
}
]
}
+108
View File
@@ -0,0 +1,108 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Multipart Payload - Basic",
"spec_ref": "spec/11-payload-multipart.md",
"protocol_version": 0,
"description": "Tests multipart payload encoding: first byte = (remaining << 4) | sub_type. Currently only multipart ACK (sub_type=3) is defined.",
"vectors": [
{
"id": "mp-001",
"description": "Multipart ACK with remaining=1, CRC 0xDEADBEEF",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "multipart",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"remaining": 1,
"sub_type": 3,
"sub_payload": "EFBEADDE"
}
},
"binary": "29 00 13 EFBEADDE",
"notes": "First byte: (1<<4)|3 = 0x13. Sub-payload: ACK CRC 0xDEADBEEF as LE."
},
{
"id": "mp-002",
"description": "Multipart ACK with remaining=0 (last in sequence)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "multipart",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"remaining": 0,
"sub_type": 3,
"sub_payload": "01000000"
}
},
"binary": "29 00 03 01000000",
"notes": "First byte: (0<<4)|3 = 0x03."
},
{
"id": "mp-003",
"description": "Multipart ACK with remaining=15 (maximum)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "multipart",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"remaining": 15,
"sub_type": 3,
"sub_payload": "FFFFFFFF"
}
},
"binary": "29 00 F3 FFFFFFFF",
"notes": "First byte: (15<<4)|3 = 0xF3."
},
{
"id": "mp-004",
"description": "Multipart ACK via direct routing",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "multipart",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 2,
"hashes": [
"AA",
"BB"
]
},
"payload": {
"remaining": 2,
"sub_type": 3,
"sub_payload": "78563412"
}
},
"binary": "2A 02 AABB 23 78563412",
"notes": "Header 0x2A = MULTIPART(10)+DIRECT(2). First byte: (2<<4)|3 = 0x23."
}
]
}
+122
View File
@@ -0,0 +1,122 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Path Return Payload - Basic",
"spec_ref": "spec/09-payload-path.md",
"protocol_version": 0,
"description": "Tests path return payload outer structure: dest_hash(1) + src_hash(1) + encrypt-then-MAC. The encrypted inner format contains path_len + path + extra_type + extra_data.",
"vectors": [
{
"id": "path-001",
"description": "Path return with encrypted inner data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "path",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "21 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
},
"notes": "Header 0x21 = PATH(8)+FLOOD(1). Same ciphertext as enc-001 since same key/plaintext. The decrypted inner format would be parsed as: path_len=0x48, hash_size=2, hash_count=8 etc. - this is a structural test only."
},
{
"id": "path-002",
"description": "Path return via DIRECT routing with 1-hop path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "path",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 1,
"hashes": ["AB"]
},
"payload": {
"dest_hash": "CD",
"src_hash": "EF",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "22 01 AB CDEF EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
},
"notes": "Header 0x22 = PATH(8)+DIRECT(2). Direct-routed path return with 1 hop."
},
{
"id": "path-003",
"description": "Path return via TRANSPORT_FLOOD with transport codes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "path",
"route_type": "transport_flood"
},
"transport_codes": [1, 2],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "EAB5",
"ciphertext": "D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "20 0100 0200 00 ABCD EAB5 D9FD218D50A4409143A7243D6D913502",
"crypto_context": {
"shared_secret": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
"plaintext": "48656C6C6F"
},
"notes": "Header 0x20 = PATH(8)+TRANSPORT_FLOOD(0). Transport codes [1, 2] as LE uint16."
},
{
"id": "path-004",
"description": "Path return with 2-block ciphertext (32 bytes plaintext)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "path",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "1234",
"ciphertext": "D9FD218D50A4409143A7243D6D913502D9FD218D50A4409143A7243D6D913502"
}
},
"binary": "21 00 ABCD 1234 D9FD218D50A4409143A7243D6D913502D9FD218D50A4409143A7243D6D913502",
"notes": "Path return with 2 AES blocks of ciphertext (32 bytes). Tests multi-block encrypted path data."
}
]
}
+97
View File
@@ -0,0 +1,97 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Raw Custom Payload - Basic",
"spec_ref": "spec/13-payload-raw-custom.md",
"protocol_version": 0,
"description": "Tests raw custom payload encoding. RAW_CUSTOM carries application-defined raw bytes.",
"vectors": [
{
"id": "raw-001",
"description": "Minimal raw custom payload (1 byte)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "42"
}
},
"binary": "3E 00 42",
"notes": "Header 0x3E = RAW_CUSTOM(15)+DIRECT(2). Single byte payload."
},
{
"id": "raw-002",
"description": "Raw custom with binary data",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 1,
"hashes": [
"AA"
]
},
"payload": {
"data": "DEADBEEFCAFEBABE"
}
},
"binary": "3E 01 AA DEADBEEFCAFEBABE",
"notes": "Direct route with 1-hop path."
},
{
"id": "raw-003",
"description": "Raw custom with all-zero payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "0000000000000000"
}
},
"binary": "3E 00 0000000000000000"
},
{
"id": "raw-004",
"description": "Raw custom with all-FF payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FFFFFFFFFFFFFFFF"
}
},
"binary": "3E 00 FFFFFFFFFFFFFFFF"
}
]
}
+127
View File
@@ -0,0 +1,127 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Trace Payload - Basic",
"spec_ref": "spec/10-payload-trace.md",
"protocol_version": 0,
"description": "Tests the basic 9-byte TRACE payload structure: tag(4) + auth_code(4) + flags(1).",
"vectors": [
{
"id": "trc-001",
"description": "Basic trace with tag=1, auth_code=2, flags=0",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"tag": 1,
"auth_code": 2,
"flags": 0
}
},
"binary": "25 00 01000000 02000000 00",
"notes": "tag=1 LE=01000000, auth_code=2 LE=02000000, flags=0."
},
{
"id": "trc-002",
"description": "Trace with large tag and auth_code values",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"tag": 4294967295,
"auth_code": 305419896,
"flags": 0
}
},
"binary": "25 00 FFFFFFFF 78563412 00",
"notes": "tag=0xFFFFFFFF, auth_code=0x12345678 LE=78563412."
},
{
"id": "trc-003",
"description": "Trace with flags indicating 1-byte path hash size (flags & 0x03 = 0)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"tag": 100,
"auth_code": 200,
"flags": 0
}
},
"binary": "25 00 64000000 C8000000 00",
"notes": "flags=0: path_hash_size = 1 << (0 & 3) = 1 byte."
},
{
"id": "trc-004",
"description": "Trace with flags indicating 2-byte path hash size (flags & 0x03 = 1)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"tag": 100,
"auth_code": 200,
"flags": 1
}
},
"binary": "25 00 64000000 C8000000 01",
"notes": "flags=1: path_hash_size = 1 << (1 & 3) = 2 bytes."
},
{
"id": "trc-005",
"description": "Trace sent via direct routing with path hashes appended to payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "01000000 02000000 00 AABBCC"
}
},
"binary": "26 00 01000000 02000000 00 AABBCC",
"notes": "Header 0x26 = TRACE(9)+DIRECT(2). path_len=0 (path field used for SNR). Payload: 9-byte header + 3 path hash bytes. Using raw payload since the appended hashes are semantically part of the trace."
}
]
}
+459
View File
@@ -0,0 +1,459 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/meshcore-spec/corpus/schema/test-vector.schema.json",
"title": "MeshCore Test Vector File",
"description": "Schema for MeshCore protocol test vector files. Each file contains a named collection of test vectors for a specific aspect of the protocol.",
"type": "object",
"required": ["name", "protocol_version", "vectors"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference"
},
"name": {
"type": "string",
"description": "Human-readable name for this test vector collection"
},
"spec_ref": {
"type": "string",
"description": "Path to the relevant spec section (e.g., 'spec/01-wire-format.md')"
},
"protocol_version": {
"type": "integer",
"minimum": 0,
"maximum": 3,
"description": "MeshCore protocol version (0-3, encoded in header bits 6-7)"
},
"description": {
"type": "string",
"description": "Optional longer description of what this file tests"
},
"vectors": {
"type": "array",
"description": "Array of test vectors",
"items": {
"$ref": "#/$defs/test_vector"
}
}
},
"$defs": {
"hex_string": {
"type": "string",
"pattern": "^[0-9A-F\\s]*$",
"description": "Hex-encoded binary data. MUST be uppercase. Whitespace allowed and stripped on parse."
},
"header": {
"type": "object",
"required": ["version", "payload_type", "route_type"],
"additionalProperties": false,
"properties": {
"version": {
"type": "integer",
"minimum": 0,
"maximum": 3,
"description": "Protocol version (header bits 6-7)"
},
"payload_type": {
"enum": [
"request",
"response",
"txt_msg",
"ack",
"advert",
"grp_txt",
"grp_data",
"anon_req",
"path",
"trace",
"multipart",
"control",
"raw_custom"
],
"description": "Payload type (header bits 2-5). Values 0x0C-0x0E are reserved."
},
"route_type": {
"enum": [
"transport_flood",
"flood",
"direct",
"transport_direct"
],
"description": "Route type (header bits 0-1). 0=transport_flood, 1=flood, 2=direct, 3=transport_direct"
}
}
},
"path_info": {
"type": "object",
"required": ["hash_size", "hash_count", "hashes"],
"additionalProperties": false,
"properties": {
"hash_size": {
"type": "integer",
"minimum": 1,
"maximum": 3,
"description": "Size of each hash in bytes (1, 2, or 3). Encoded as (hash_size - 1) in bits 6-7 of path_len byte."
},
"hash_count": {
"type": "integer",
"minimum": 0,
"maximum": 63,
"description": "Number of hashes in path (bits 0-5 of path_len byte)"
},
"hashes": {
"type": "array",
"items": {
"$ref": "#/$defs/hex_string"
},
"description": "Array of hex-encoded path hashes. Length MUST equal hash_count. Each hash MUST be hash_size bytes."
}
}
},
"transport_codes": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"minItems": 2,
"maxItems": 2,
"description": "Two 16-bit transport codes [code1, code2], encoded as little-endian uint16"
},
"payload_ack": {
"type": "object",
"required": ["ack_crc"],
"additionalProperties": false,
"properties": {
"ack_crc": {
"$ref": "#/$defs/hex_string",
"description": "4-byte ACK CRC as hex (big-endian representation of the value, wire encoding is little-endian)"
}
}
},
"payload_advert": {
"type": "object",
"required": ["pub_key", "timestamp", "signature"],
"additionalProperties": false,
"properties": {
"pub_key": {
"$ref": "#/$defs/hex_string",
"description": "32-byte Ed25519 public key"
},
"timestamp": {
"type": "integer",
"minimum": 0,
"description": "4-byte Unix timestamp"
},
"signature": {
"$ref": "#/$defs/hex_string",
"description": "64-byte Ed25519 signature over (pub_key + timestamp_le + app_data)"
},
"app_data": {
"$ref": "#/$defs/advert_app_data",
"description": "Optional advertisement application data"
}
}
},
"advert_app_data": {
"type": "object",
"required": ["flags"],
"additionalProperties": false,
"properties": {
"flags": {
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "8-bit flags: bits 0-3 = type (0=None,1=Chat,2=Repeater,3=Room,4=Sensor), bit 4 = has_location, bit 5 = has_feat1, bit 6 = has_feat2, bit 7 = has_name"
},
"latitude": {
"type": "integer",
"description": "Latitude * 1,000,000 as int32 (present when flags bit 4 set)"
},
"longitude": {
"type": "integer",
"description": "Longitude * 1,000,000 as int32 (present when flags bit 4 set)"
},
"feat1": {
"type": "integer",
"minimum": 0,
"maximum": 65535,
"description": "Feature field 1 as uint16 (present when flags bit 5 set)"
},
"feat2": {
"type": "integer",
"minimum": 0,
"maximum": 65535,
"description": "Feature field 2 as uint16 (present when flags bit 6 set)"
},
"name": {
"type": "string",
"description": "Node name as UTF-8 string (present when flags bit 7 set)"
}
}
},
"payload_encrypted": {
"type": "object",
"required": ["dest_hash", "src_hash", "cipher_mac", "ciphertext"],
"additionalProperties": false,
"properties": {
"dest_hash": {
"$ref": "#/$defs/hex_string",
"description": "Destination hash (PATH_HASH_SIZE bytes, default 1)"
},
"src_hash": {
"$ref": "#/$defs/hex_string",
"description": "Source hash (PATH_HASH_SIZE bytes, default 1)"
},
"cipher_mac": {
"$ref": "#/$defs/hex_string",
"description": "2-byte HMAC-SHA256 truncated MAC"
},
"ciphertext": {
"$ref": "#/$defs/hex_string",
"description": "AES-128-ECB encrypted data (zero-padded to block boundary)"
}
}
},
"payload_anon_req": {
"type": "object",
"required": ["dest_hash", "sender_pub_key", "cipher_mac", "ciphertext"],
"additionalProperties": false,
"properties": {
"dest_hash": {
"$ref": "#/$defs/hex_string",
"description": "Destination hash (PATH_HASH_SIZE bytes)"
},
"sender_pub_key": {
"$ref": "#/$defs/hex_string",
"description": "32-byte Ed25519 public key of sender"
},
"cipher_mac": {
"$ref": "#/$defs/hex_string",
"description": "2-byte cipher MAC"
},
"ciphertext": {
"$ref": "#/$defs/hex_string",
"description": "Encrypted data"
}
}
},
"payload_group": {
"type": "object",
"required": ["channel_hash", "cipher_mac", "ciphertext"],
"additionalProperties": false,
"properties": {
"channel_hash": {
"$ref": "#/$defs/hex_string",
"description": "1-byte channel hash (first byte of SHA-256 of channel shared key)"
},
"cipher_mac": {
"$ref": "#/$defs/hex_string",
"description": "2-byte cipher MAC"
},
"ciphertext": {
"$ref": "#/$defs/hex_string",
"description": "Encrypted data"
}
}
},
"payload_trace": {
"type": "object",
"required": ["tag", "auth_code", "flags"],
"additionalProperties": false,
"properties": {
"tag": {
"type": "integer",
"minimum": 0,
"description": "4-byte random tag set by initiator"
},
"auth_code": {
"type": "integer",
"minimum": 0,
"description": "4-byte authentication code"
},
"flags": {
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "1-byte flags (bits 0-1 encode path hash size)"
}
}
},
"payload_multipart": {
"type": "object",
"required": ["remaining", "sub_type", "sub_payload"],
"additionalProperties": false,
"properties": {
"remaining": {
"type": "integer",
"minimum": 0,
"maximum": 15,
"description": "Remaining parts count (upper 4 bits of first byte)"
},
"sub_type": {
"type": "integer",
"minimum": 0,
"maximum": 15,
"description": "Sub-payload type (lower 4 bits of first byte)"
},
"sub_payload": {
"$ref": "#/$defs/hex_string",
"description": "Sub-payload data"
}
}
},
"payload_control": {
"type": "object",
"required": ["data"],
"additionalProperties": false,
"properties": {
"data": {
"$ref": "#/$defs/hex_string",
"description": "Raw control data bytes"
}
}
},
"payload_raw": {
"type": "object",
"required": ["data"],
"additionalProperties": false,
"properties": {
"data": {
"$ref": "#/$defs/hex_string",
"description": "Raw custom payload bytes"
}
}
},
"crypto_context": {
"type": "object",
"additionalProperties": false,
"properties": {
"sender_private_key": {
"$ref": "#/$defs/hex_string",
"description": "64-byte Ed25519 private key of sender"
},
"sender_public_key": {
"$ref": "#/$defs/hex_string",
"description": "32-byte Ed25519 public key of sender"
},
"recipient_private_key": {
"$ref": "#/$defs/hex_string",
"description": "64-byte Ed25519 private key of recipient"
},
"recipient_public_key": {
"$ref": "#/$defs/hex_string",
"description": "32-byte Ed25519 public key of recipient"
},
"shared_secret": {
"$ref": "#/$defs/hex_string",
"description": "32-byte ECDH shared secret"
},
"encryption_key": {
"$ref": "#/$defs/hex_string",
"description": "16-byte AES-128 key (first 16 bytes of shared secret)"
},
"plaintext": {
"$ref": "#/$defs/hex_string",
"description": "Plaintext before encryption"
}
}
},
"structured_packet": {
"type": "object",
"required": ["header", "path"],
"additionalProperties": false,
"properties": {
"header": {
"$ref": "#/$defs/header"
},
"transport_codes": {
"$ref": "#/$defs/transport_codes"
},
"path": {
"$ref": "#/$defs/path_info"
},
"payload": {
"description": "Payload structure — the schema depends on the payload_type in header",
"anyOf": [
{ "$ref": "#/$defs/payload_ack" },
{ "$ref": "#/$defs/payload_advert" },
{ "$ref": "#/$defs/payload_encrypted" },
{ "$ref": "#/$defs/payload_anon_req" },
{ "$ref": "#/$defs/payload_group" },
{ "$ref": "#/$defs/payload_trace" },
{ "$ref": "#/$defs/payload_multipart" },
{ "$ref": "#/$defs/payload_control" },
{ "$ref": "#/$defs/payload_raw" }
]
}
}
},
"test_vector": {
"type": "object",
"required": ["id", "description", "type"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9_-]*$",
"description": "Unique identifier within this file (e.g., 'ack-001')"
},
"description": {
"type": "string",
"description": "Human-readable description of what this vector tests"
},
"type": {
"enum": ["encode_decode", "decode_only", "invalid"],
"description": "Test mode: encode_decode (bidirectional), decode_only (binary->structured), invalid (must reject)"
},
"structured": {
"$ref": "#/$defs/structured_packet",
"description": "Structured packet representation (required for encode_decode and decode_only)"
},
"binary": {
"$ref": "#/$defs/hex_string",
"description": "Wire-format binary encoding as hex (required for all types)"
},
"crypto_context": {
"$ref": "#/$defs/crypto_context",
"description": "Cryptographic key material for encrypted payload vectors"
},
"expected_error": {
"type": "string",
"description": "Expected error category for invalid vectors"
},
"notes": {
"type": "string",
"description": "Optional notes explaining the test vector"
}
},
"allOf": [
{
"if": {
"properties": { "type": { "const": "encode_decode" } }
},
"then": {
"required": ["structured", "binary"]
}
},
{
"if": {
"properties": { "type": { "const": "decode_only" } }
},
"then": {
"required": ["structured", "binary"]
}
},
{
"if": {
"properties": { "type": { "const": "invalid" } }
},
"then": {
"required": ["binary"]
}
}
]
}
}
}
@@ -0,0 +1,75 @@
{
"$schema": "../../../schema/test-vector.schema.json",
"name": "Framing - Decode Only",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests one-way decode cases where the binary is valid to decode but may not be the canonical encoding a conformant encoder would produce.",
"vectors": [
{
"id": "dec-001",
"description": "Decode-only: ACK FLOOD with trailing bytes beyond the 4-byte CRC payload",
"type": "decode_only",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "DEADBEEF"
}
},
"binary": "0D 00 DEADBEEF 00",
"notes": "Binary has 1 trailing byte after the 4-byte ACK CRC. Decoders MUST accept this (payload_len includes the extra byte). Encoders would not produce trailing bytes."
},
{
"id": "dec-002",
"description": "Decode-only: raw_custom FLOOD with path hash_count=0 but hash_size=2",
"type": "decode_only",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "AA"
}
},
"binary": "3D 40 AA",
"notes": "Path_len byte 0x40 encodes hash_size=2 (bits 6-7 = 01) with hash_count=0. Decoders must handle non-default hash_size even when hash_count is zero. Encoders typically default hash_size=1 when hash_count=0."
},
{
"id": "dec-003",
"description": "Decode-only: raw_custom FLOOD with path hash_size=3 and hash_count=0",
"type": "decode_only",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "BB"
}
},
"binary": "3D 80 BB",
"notes": "Path_len byte 0x80 encodes hash_size=3 (bits 6-7 = 10) with hash_count=0. Same as dec-002 but with hash_size=3."
}
]
}
@@ -0,0 +1,33 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Framing - Empty Payload Rejection",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Verifies that packets with zero-length payload are rejected. The reference implementation (Packet::readFrom) checks 'if (i >= len) return false' after reading header+transport+path_len+path, requiring at least 1 byte of payload.",
"vectors": [
{
"id": "ep-001",
"description": "FLOOD packet with no payload bytes (just header + path_len)",
"type": "invalid",
"binary": "0D 00",
"expected_error": "empty_payload",
"notes": "2 bytes total: header 0x0D + path_len 0x00. No payload bytes remain. readFrom returns false because i(2) >= len(2)."
},
{
"id": "ep-002",
"description": "TRANSPORT_FLOOD with no payload (header + transport + path_len only)",
"type": "invalid",
"binary": "0C 0000 0000 00",
"expected_error": "empty_payload",
"notes": "6 bytes: header(1) + transport(4) + path_len(1). i=6, len=6, so i>=len = true."
},
{
"id": "ep-003",
"description": "FLOOD packet with path but no payload",
"type": "invalid",
"binary": "0D 02 AABB",
"expected_error": "empty_payload",
"notes": "4 bytes: header(1) + path_len(1) + path(2). i=4, len=4."
}
]
}
+139
View File
@@ -0,0 +1,139 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Framing - Maximum Size Packets",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests packets at or near the maximum wire size of 255 bytes (MAX_TRANS_UNIT). Verifies size calculation: total = 1 (header) + transport(0|4) + 1 (path_len) + path_bytes + payload_len.",
"vectors": [
{
"id": "max-001",
"description": "Maximum FLOOD packet: 255 bytes with empty path and 253-byte payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFC"
}
},
"binary": "3D 00 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFC",
"notes": "Total: 1 + 0 + 1 + 0 + 253 = 255 bytes. However payload exceeds MAX_PACKET_PAYLOAD (184). This is a wire-level max size test only; payload-level validation would reject this."
},
{
"id": "max-002",
"description": "Maximum valid FLOOD packet respecting MAX_PACKET_PAYLOAD: 1 + 1 + 0 + 184 = 186 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7"
}
},
"binary": "3D 00 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7",
"notes": "Total: 1 + 1 + 184 = 186 bytes. Payload is exactly MAX_PACKET_PAYLOAD (184 bytes = 0x00..0xB7)."
},
{
"id": "max-003",
"description": "Large FLOOD packet with maximum path (63 bytes) and payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 63,
"hashes": [
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"0A",
"0B",
"0C",
"0D",
"0E",
"0F",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"1A",
"1B",
"1C",
"1D",
"1E",
"1F",
"20",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"29",
"2A",
"2B",
"2C",
"2D",
"2E",
"2F",
"30",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38",
"39",
"3A",
"3B",
"3C",
"3D",
"3E",
"3F"
]
},
"payload": {
"data": "DEADBEEF"
}
},
"binary": "3D 3F 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F DEADBEEF",
"notes": "Total: 1 + 1 + 63 + 4 = 69 bytes. Max path with small payload."
}
]
}
+127
View File
@@ -0,0 +1,127 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Framing - Minimal Valid Packets",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests the smallest valid packets for each route type. A valid packet must have at least header(1) + path_len(1) + payload(1+) bytes, plus 4 bytes for transport codes if applicable.",
"vectors": [
{
"id": "min-001",
"description": "Minimal FLOOD packet: 3 bytes (header + path_len + 1-byte payload)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "3D 00 FF",
"notes": "Smallest valid FLOOD packet: 3 bytes total."
},
{
"id": "min-002",
"description": "Minimal DIRECT packet: 3 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "3E 00 FF",
"notes": "Smallest valid DIRECT packet: 3 bytes total."
},
{
"id": "min-003",
"description": "Minimal TRANSPORT_FLOOD packet: 7 bytes (header + 4 transport + path_len + 1-byte payload)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "transport_flood"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "3C 0000 0000 00 FF",
"notes": "Smallest valid TRANSPORT_FLOOD packet: 7 bytes total."
},
{
"id": "min-004",
"description": "Minimal TRANSPORT_DIRECT packet: 7 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "transport_direct"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "3F 0000 0000 00 FF",
"notes": "Smallest valid TRANSPORT_DIRECT packet: 7 bytes total."
},
{
"id": "min-005",
"description": "Minimal ACK via FLOOD: 6 bytes (header + path_len + 4-byte CRC)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000000"
}
},
"binary": "0D 00 00000000",
"notes": "ACK with zero CRC. 6 bytes total."
}
]
}
+165
View File
@@ -0,0 +1,165 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Header - Combined Field Encoding",
"spec_ref": "spec/02-header.md",
"protocol_version": 0,
"description": "Combinatorial tests of header byte encoding, exercising various combinations of version, payload type, and route type to verify correct bit packing.",
"vectors": [
{
"id": "hdr-000",
"description": "All-zero header: version=0, request=0, transport_flood=0",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "request",
"route_type": "transport_flood"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "00",
"src_hash": "00",
"cipher_mac": "0000",
"ciphertext": "00000000000000000000000000000000"
}
},
"binary": "00 0000 0000 00 0000 0000 00000000000000000000000000000000",
"notes": "Header: (0<<6)|(0<<2)|0 = 0x00."
},
{
"id": "hdr-001",
"description": "Version=0, ADVERT, DIRECT route",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 2,
"hashes": [
"AA",
"BB"
]
},
"payload": {
"data": "FF"
}
},
"binary": "12 02 AABB FF",
"notes": "Header: (0<<6)|(4<<2)|2 = 0x12. path_len=0x02 (hash_size=1, count=2). Path: AA BB."
},
{
"id": "hdr-002",
"description": "Version=1, CONTROL, FLOOD route",
"type": "encode_decode",
"structured": {
"header": {
"version": 1,
"payload_type": "control",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "6D 00 FF",
"notes": "Header: (1<<6)|(11<<2)|1 = 0x40+0x2C+0x01 = 0x6D."
},
{
"id": "hdr-003",
"description": "Version=0, RAW_CUSTOM, TRANSPORT_DIRECT with non-trivial transport codes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "transport_direct"
},
"transport_codes": [
256,
512
],
"path": {
"hash_size": 1,
"hash_count": 1,
"hashes": [
"AA"
]
},
"payload": {
"data": "CAFEBABE"
}
},
"binary": "3F 0001 0002 01 AA CAFEBABE",
"notes": "Header: (0<<6)|(15<<2)|3 = 0x3F. Transport codes: 256=0x0100 LE=0001, 512=0x0200 LE=0002."
},
{
"id": "hdr-004",
"description": "Version=0, TRACE, FLOOD with path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 3,
"hashes": [
"AA",
"BB",
"CC"
]
},
"payload": {
"data": "0102030405060708 09"
}
},
"binary": "25 03 AABBCC 010203040506070809",
"notes": "Header: (0<<6)|(9<<2)|1 = 0x25. path_len=0x03 (1-byte hashes, 3 hops)."
},
{
"id": "hdr-005",
"description": "Maximum header value that isn't 0xFF sentinel: version=2, RAW_CUSTOM, TRANSPORT_DIRECT",
"type": "encode_decode",
"structured": {
"header": {
"version": 2,
"payload_type": "raw_custom",
"route_type": "transport_direct"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "AA"
}
},
"binary": "BF 0000 0000 00 AA",
"notes": "Header: (2<<6)|(15<<2)|3 = 0x80+0x3C+0x03 = 0xBF."
}
]
}
@@ -0,0 +1,308 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Header - Payload Types",
"spec_ref": "spec/02-header.md",
"protocol_version": 0,
"description": "Tests encoding of all defined payload types in the header byte. Uses FLOOD routing and minimal payloads to isolate header encoding.",
"vectors": [
{
"id": "pt-000",
"description": "Payload type REQUEST (0x00) - header byte encodes type in bits 2-5",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "request",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "1122",
"ciphertext": "DEADBEEFCAFEBABE0102030405060708"
}
},
"binary": "01 00 ABCD 1122 DEADBEEFCAFEBABE0102030405060708",
"notes": "Header: (0<<6)|(0<<2)|1 = 0x01. Payload: dest_hash(1) + src_hash(1) + MAC(2) + 16 bytes ciphertext."
},
{
"id": "pt-001",
"description": "Payload type RESPONSE (0x01)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "response",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "1122",
"ciphertext": "DEADBEEFCAFEBABE0102030405060708"
}
},
"binary": "05 00 ABCD 1122 DEADBEEFCAFEBABE0102030405060708",
"notes": "Header: (0<<6)|(1<<2)|1 = 0x05."
},
{
"id": "pt-002",
"description": "Payload type TXT_MSG (0x02)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "txt_msg",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"dest_hash": "AB",
"src_hash": "CD",
"cipher_mac": "1122",
"ciphertext": "DEADBEEFCAFEBABE0102030405060708"
}
},
"binary": "09 00 ABCD 1122 DEADBEEFCAFEBABE0102030405060708",
"notes": "Header: (0<<6)|(2<<2)|1 = 0x09."
},
{
"id": "pt-003",
"description": "Payload type ACK (0x03)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "DEADBEEF"
}
},
"binary": "0D 00 EFBEADDE",
"notes": "Header: (0<<6)|(3<<2)|1 = 0x0D. CRC 0xDEADBEEF as LE = EFBEADDE."
},
{
"id": "pt-004",
"description": "Payload type ADVERT (0x04) - minimal advert with 1 byte payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "advert",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "11 00 FF",
"notes": "Header: (0<<6)|(4<<2)|1 = 0x11. Using raw payload to test header encoding in isolation."
},
{
"id": "pt-005",
"description": "Payload type GRP_TXT (0x05)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "grp_txt",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"channel_hash": "AB",
"cipher_mac": "1122",
"ciphertext": "DEADBEEFCAFEBABE0102030405060708"
}
},
"binary": "15 00 AB 1122 DEADBEEFCAFEBABE0102030405060708",
"notes": "Header: (0<<6)|(5<<2)|1 = 0x15."
},
{
"id": "pt-006",
"description": "Payload type GRP_DATA (0x06)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "grp_data",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"channel_hash": "AB",
"cipher_mac": "1122",
"ciphertext": "DEADBEEFCAFEBABE0102030405060708"
}
},
"binary": "19 00 AB 1122 DEADBEEFCAFEBABE0102030405060708",
"notes": "Header: (0<<6)|(6<<2)|1 = 0x19."
},
{
"id": "pt-007",
"description": "Payload type ANON_REQ (0x07)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "anon_req",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "1D 00 FF",
"notes": "Header: (0<<6)|(7<<2)|1 = 0x1D. Minimal payload to test header."
},
{
"id": "pt-008",
"description": "Payload type PATH (0x08)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "path",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "21 00 FF",
"notes": "Header: (0<<6)|(8<<2)|1 = 0x21."
},
{
"id": "pt-009",
"description": "Payload type TRACE (0x09)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "trace",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "25 00 FF",
"notes": "Header: (0<<6)|(9<<2)|1 = 0x25."
},
{
"id": "pt-010",
"description": "Payload type MULTIPART (0x0A)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "multipart",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "29 00 FF",
"notes": "Header: (0<<6)|(10<<2)|1 = 0x29."
},
{
"id": "pt-011",
"description": "Payload type CONTROL (0x0B)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "control",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "FF"
}
},
"binary": "2D 00 FF",
"notes": "Header: (0<<6)|(11<<2)|1 = 0x2D."
},
{
"id": "pt-012",
"description": "Payload type RAW_CUSTOM (0x0F)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "raw_custom",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"data": "CAFEBABE"
}
},
"binary": "3D 00 CAFEBABE",
"notes": "Header: (0<<6)|(15<<2)|1 = 0x3D."
}
]
}
+131
View File
@@ -0,0 +1,131 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Header - Route Types",
"spec_ref": "spec/02-header.md",
"protocol_version": 0,
"description": "Tests encoding and decoding of all 4 route types in the header byte, using ACK (payload_type=3) as a simple payload.",
"vectors": [
{
"id": "rt-000",
"description": "Route type TRANSPORT_FLOOD (0x00) with ACK payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_flood"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "01020304"
}
},
"binary": "0C 0000 0000 00 04030201",
"notes": "Header: version=0, payload_type=3 (ACK), route_type=0 (TRANSPORT_FLOOD). 0x0C = (0<<6)|(3<<2)|0. Transport codes both zero. path_len=0x00 (hash_size=1, count=0). ACK CRC 0x01020304 as LE = 04030201."
},
{
"id": "rt-001",
"description": "Route type FLOOD (0x01) with ACK payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "01020304"
}
},
"binary": "0D 00 04030201",
"notes": "Header: 0x0D = (0<<6)|(3<<2)|1. No transport codes. path_len=0x00. ACK CRC as LE."
},
{
"id": "rt-002",
"description": "Route type DIRECT (0x02) with ACK payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "01020304"
}
},
"binary": "0E 00 04030201",
"notes": "Header: 0x0E = (0<<6)|(3<<2)|2. No transport codes."
},
{
"id": "rt-003",
"description": "Route type TRANSPORT_DIRECT (0x03) with ACK payload",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_direct"
},
"transport_codes": [
4660,
22136
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "01020304"
}
},
"binary": "0F 3412 7856 00 04030201",
"notes": "Header: 0x0F = (0<<6)|(3<<2)|3. Transport codes: 4660=0x1234 as LE=3412, 22136=0x5678 as LE=7856."
},
{
"id": "rt-004",
"description": "TRANSPORT_FLOOD with non-zero transport codes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_flood"
},
"transport_codes": [
65535,
1
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "FFFFFFFF"
}
},
"binary": "0C FFFF 0100 00 FFFFFFFF",
"notes": "Transport code 1: 0xFFFF as LE = FFFF. Transport code 2: 1 as LE = 0100. CRC 0xFFFFFFFF as LE."
}
]
}
+97
View File
@@ -0,0 +1,97 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Header - Protocol Versions",
"spec_ref": "spec/02-header.md",
"protocol_version": 0,
"description": "Tests encoding of all 4 protocol version values in header bits 6-7. Uses ACK (type=3) with FLOOD routing as a simple baseline.",
"vectors": [
{
"id": "ver-000",
"description": "Version 0 (V1, current) - bits 6-7 = 00",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 00 01000000",
"notes": "Header: (0<<6)|(3<<2)|1 = 0x0D. Version 0 = V1."
},
{
"id": "ver-001",
"description": "Version 1 (V2, reserved) - bits 6-7 = 01",
"type": "encode_decode",
"structured": {
"header": {
"version": 1,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "4D 00 01000000",
"notes": "Header: (1<<6)|(3<<2)|1 = 0x4D. Version 1 = V2 (reserved)."
},
{
"id": "ver-002",
"description": "Version 2 (V3, reserved) - bits 6-7 = 10",
"type": "encode_decode",
"structured": {
"header": {
"version": 2,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "8D 00 01000000",
"notes": "Header: (2<<6)|(3<<2)|1 = 0x8D. Version 2 = V3 (reserved)."
},
{
"id": "ver-003",
"description": "Version 3 (V4, reserved) - bits 6-7 = 11",
"type": "encode_decode",
"structured": {
"header": {
"version": 3,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "CD 00 01000000",
"notes": "Header: (3<<6)|(3<<2)|1 = 0xCD. Version 3 = V4 (reserved)."
}
]
}
@@ -0,0 +1,57 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Invalid - Bad Path Length Encoding",
"spec_ref": "spec/03-path.md",
"protocol_version": 0,
"description": "Tests invalid path_len values including the reserved hash_size=4 and path overflow conditions.",
"vectors": [
{
"id": "bpl-001",
"description": "Reserved hash size code 3 (hash_size would be 4): path_len=0xC0",
"type": "invalid",
"binary": "0D C0 01000000",
"expected_error": "reserved_hash_size",
"notes": "path_len=0xC0: bits 6-7=11 (hash size code 3). isValidPathLen returns false because hash_size==4."
},
{
"id": "bpl-002",
"description": "Reserved hash size code 3 with non-zero count: path_len=0xC1",
"type": "invalid",
"binary": "0D C1 AA 01000000",
"expected_error": "reserved_hash_size",
"notes": "path_len=0xC1: hash size code 3 (reserved), count=1. Invalid regardless of count."
},
{
"id": "bpl-003",
"description": "Reserved hash size code 3, max count: path_len=0xFF",
"type": "invalid",
"binary": "0D FF 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F 01000000",
"expected_error": "reserved_hash_size",
"notes": "path_len=0xFF: hash size code 3 (reserved), count=63. Even if hash_size=4 were valid, 63*4=252 > MAX_PATH_SIZE."
},
{
"id": "bpl-004",
"description": "Path overflow: hash_size=2, count=33 (66 bytes > MAX_PATH_SIZE)",
"type": "invalid",
"binary": "0D 61 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536373839 3A3B3C3D3E3F4041 01000000",
"expected_error": "path_overflow",
"notes": "path_len=0x61: hash_size=2, count=33. 33*2=66 > MAX_PATH_SIZE (64). isValidPathLen returns false."
},
{
"id": "bpl-005",
"description": "Path overflow: hash_size=3, count=22 (66 bytes > MAX_PATH_SIZE)",
"type": "invalid",
"binary": "0D 96 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F4041 01000000",
"expected_error": "path_overflow",
"notes": "path_len=0x96: hash_size=3, count=22. 22*3=66 > MAX_PATH_SIZE (64)."
},
{
"id": "bpl-006",
"description": "Path overflow edge case: hash_size=1, count=63 is valid (63<=64), but with hash_size=2 and count=33 it overflows",
"type": "invalid",
"binary": "0D 61 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334353637383940 4142 DEADBEEF",
"expected_error": "path_overflow",
"notes": "path_len=0x61: size code 1 (hash_size=2), count 33. 33*2=66 > 64."
}
]
}
+49
View File
@@ -0,0 +1,49 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Invalid - Too Short Packets",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests packets that are shorter than the minimum required size for their route type.",
"vectors": [
{
"id": "short-001",
"description": "1-byte packet (FLOOD): only header, missing path_len and payload",
"type": "invalid",
"binary": "0D",
"expected_error": "too_short",
"notes": "Minimum FLOOD packet is 3 bytes. This is 1."
},
{
"id": "short-002",
"description": "2-byte packet (FLOOD): header + path_len, missing payload",
"type": "invalid",
"binary": "0D 00",
"expected_error": "empty_payload",
"notes": "Header + path_len present but no payload. i=2, len=2, so i>=len."
},
{
"id": "short-003",
"description": "5-byte TRANSPORT_FLOOD: header + transport codes, missing path_len",
"type": "invalid",
"binary": "0C 00000000",
"expected_error": "too_short",
"notes": "TRANSPORT_FLOOD needs at least 7 bytes (1+4+1+1). This has 5."
},
{
"id": "short-004",
"description": "6-byte TRANSPORT_FLOOD: header + transport + path_len, missing payload",
"type": "invalid",
"binary": "0C 00000000 00",
"expected_error": "empty_payload",
"notes": "All framing present but zero payload."
},
{
"id": "short-005",
"description": "FLOOD with path consuming all remaining bytes",
"type": "invalid",
"binary": "0D 03 AABBCC",
"expected_error": "empty_payload",
"notes": "header(1) + path_len(1) + path(3) = 5 bytes = total. i=5, len=5."
}
]
}
+65
View File
@@ -0,0 +1,65 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Invalid - Truncated Packets",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests packets that are truncated at various points, verifying that decoders reject them gracefully.",
"vectors": [
{
"id": "trunc-001",
"description": "Empty packet (0 bytes)",
"type": "invalid",
"binary": "",
"expected_error": "too_short",
"notes": "Zero-length input. Cannot read header."
},
{
"id": "trunc-002",
"description": "Header only (1 byte, FLOOD route)",
"type": "invalid",
"binary": "0D",
"expected_error": "too_short",
"notes": "Only header byte. Cannot read path_len."
},
{
"id": "trunc-003",
"description": "Header only (1 byte, TRANSPORT_FLOOD route) - transport codes truncated",
"type": "invalid",
"binary": "0C",
"expected_error": "too_short",
"notes": "Route type 0x00 requires 4 transport code bytes, but only header is present."
},
{
"id": "trunc-004",
"description": "TRANSPORT_FLOOD with partial transport codes (2 of 4 bytes)",
"type": "invalid",
"binary": "0C 1234",
"expected_error": "too_short",
"notes": "Only 2 of 4 transport code bytes present."
},
{
"id": "trunc-005",
"description": "TRANSPORT_FLOOD with transport codes but no path_len",
"type": "invalid",
"binary": "0C 0000 0000",
"expected_error": "too_short",
"notes": "5 bytes: header + 4 transport code bytes. Missing path_len."
},
{
"id": "trunc-006",
"description": "FLOOD with path_len indicating 3 hops but only 1 path byte",
"type": "invalid",
"binary": "0D 03 AA FF",
"expected_error": "truncated_path",
"notes": "path_len=0x03 means 3 bytes of path expected but only 1 byte follows before the alleged payload. The decoder would read 3 path bytes (AA FF ??) and run out."
},
{
"id": "trunc-007",
"description": "FLOOD with 2-byte hash path_len=0x42 (4 path bytes needed) but only 2 available",
"type": "invalid",
"binary": "0D 42 AABB FF",
"expected_error": "truncated_path",
"notes": "path_len=0x42: hash_size=2, count=2, need 4 path bytes. Only 2 bytes before payload marker."
}
]
}
+156
View File
@@ -0,0 +1,156 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Path - Boundary Conditions",
"spec_ref": "spec/03-path.md",
"protocol_version": 0,
"description": "Tests boundary conditions for path encoding including maximum valid values and values just below overflow thresholds.",
"vectors": [
{
"id": "pb-001",
"description": "hash_size=2, count=31 (62 bytes, 2 below max)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 31,
"hashes": [
"0001",
"0203",
"0405",
"0607",
"0809",
"0A0B",
"0C0D",
"0E0F",
"1011",
"1213",
"1415",
"1617",
"1819",
"1A1B",
"1C1D",
"1E1F",
"2021",
"2223",
"2425",
"2627",
"2829",
"2A2B",
"2C2D",
"2E2F",
"3031",
"3233",
"3435",
"3637",
"3839",
"3A3B",
"3C3D"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 5F 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D 01000000",
"notes": "path_len=0x5F: hash_size=2, count=31. Path: 62 bytes. Just below max."
},
{
"id": "pb-002",
"description": "hash_size=3, count=20 (60 bytes, 3 below max for this hash_size)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 20,
"hashes": [
"000102",
"030405",
"060708",
"090A0B",
"0C0D0E",
"0F1011",
"121314",
"151617",
"18191A",
"1B1C1D",
"1E1F20",
"212223",
"242526",
"272829",
"2A2B2C",
"2D2E2F",
"303132",
"333435",
"363738",
"393A3B"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 94 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233343536373839 3A3B 01000000",
"notes": "path_len=0x94: hash_size=3, count=20. Path: 60 bytes."
},
{
"id": "pb-003",
"description": "Single-byte path: hash_size=1, count=1",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 1,
"hashes": [
"00"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 01 00 01000000",
"notes": "Minimal non-empty path. Hash value 0x00 is valid."
},
{
"id": "pb-004",
"description": "Path with all-FF hashes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 3,
"hashes": [
"FF",
"FF",
"FF"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 03 FFFFFF 01000000",
"notes": "All hashes are 0xFF. Valid encoding."
}
]
}
+260
View File
@@ -0,0 +1,260 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Path - Hash Counts",
"spec_ref": "spec/03-path.md",
"protocol_version": 0,
"description": "Tests various hash count values including zero, small, and maximum values for each hash size.",
"vectors": [
{
"id": "hc-000",
"description": "Zero hops (hash_count=0, hash_size=1) - empty path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 00 01000000",
"notes": "path_len=0x00. Zero-length path."
},
{
"id": "hc-001",
"description": "Zero hops with hash_size=2 - still empty path",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 40 01000000",
"notes": "path_len=0x40: hash_size=2, count=0. Path is still 0 bytes even though hash_size>1."
},
{
"id": "hc-002",
"description": "Zero hops with hash_size=3",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 80 01000000",
"notes": "path_len=0x80: hash_size=3, count=0. Path is 0 bytes."
},
{
"id": "hc-010",
"description": "Maximum count for hash_size=1: 63 hops, 63 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 63,
"hashes": [
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"0A",
"0B",
"0C",
"0D",
"0E",
"0F",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"1A",
"1B",
"1C",
"1D",
"1E",
"1F",
"20",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"29",
"2A",
"2B",
"2C",
"2D",
"2E",
"2F",
"30",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38",
"39",
"3A",
"3B",
"3C",
"3D",
"3E",
"3F"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 3F 0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F 01000000",
"notes": "path_len=0x3F: hash_size=1, count=63. Path: 63 bytes. Maximum valid path for 1-byte hashes."
},
{
"id": "hc-020",
"description": "Maximum count for hash_size=2: 32 hops, 64 bytes (exactly MAX_PATH_SIZE)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 32,
"hashes": [
"0001",
"0203",
"0405",
"0607",
"0809",
"0A0B",
"0C0D",
"0E0F",
"1011",
"1213",
"1415",
"1617",
"1819",
"1A1B",
"1C1D",
"1E1F",
"2021",
"2223",
"2425",
"2627",
"2829",
"2A2B",
"2C2D",
"2E2F",
"3031",
"3233",
"3435",
"3637",
"3839",
"3A3B",
"3C3D",
"3E3F"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 60 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F 01000000",
"notes": "path_len=0x60: bits 6-7=01 (hash_size=2), bits 0-5=32. Path: 64 bytes = MAX_PATH_SIZE. Maximum valid for 2-byte hashes."
},
{
"id": "hc-030",
"description": "Maximum count for hash_size=3: 21 hops, 63 bytes",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 21,
"hashes": [
"000102",
"030405",
"060708",
"090A0B",
"0C0D0E",
"0F1011",
"121314",
"151617",
"18191A",
"1B1C1D",
"1E1F20",
"212223",
"242526",
"272829",
"2A2B2C",
"2D2E2F",
"303132",
"333435",
"363738",
"393A3B",
"3C3D3E"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 95 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E 01000000",
"notes": "path_len=0x95: bits 6-7=10 (hash_size=3), bits 0-5=21. Path: 63 bytes. Maximum valid for 3-byte hashes (22*3=66 would overflow)."
}
]
}
+188
View File
@@ -0,0 +1,188 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Path - Hash Sizes",
"spec_ref": "spec/03-path.md",
"protocol_version": 0,
"description": "Tests all valid hash sizes (1, 2, 3 bytes) in the path length encoding. These vectors are critical for catching implementations that treat path_len as a raw byte count.",
"vectors": [
{
"id": "hs-001",
"description": "Hash size 1 (code 0), 1 hop",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 1,
"hashes": [
"AA"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 01 AA 01000000",
"notes": "path_len=0x01: bits 6-7=00 (size code 0, hash_size=1), bits 0-5=1 (count=1). Path: 1 byte."
},
{
"id": "hs-002",
"description": "Hash size 1 (code 0), 5 hops",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 5,
"hashes": [
"AA",
"BB",
"CC",
"DD",
"EE"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 05 AABBCCDDEE 01000000",
"notes": "path_len=0x05: hash_size=1, count=5. Path: 5 bytes."
},
{
"id": "hs-010",
"description": "Hash size 2 (code 1), 1 hop",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 1,
"hashes": [
"AABB"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 41 AABB 01000000",
"notes": "path_len=0x41: bits 6-7=01 (size code 1, hash_size=2), bits 0-5=1 (count=1). Path: 2 bytes. An implementation treating 0x41=65 as raw byte count would fail."
},
{
"id": "hs-011",
"description": "Hash size 2 (code 1), 2 hops",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 2,
"hashes": [
"AABB",
"CCDD"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 42 AABBCCDD 01000000",
"notes": "path_len=0x42: hash_size=2, count=2. Path: 4 bytes. This is the example that catches the meshcore-rs bug (0x42=66 raw vs 4 actual bytes)."
},
{
"id": "hs-012",
"description": "Hash size 2 (code 1), 5 hops",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 2,
"hash_count": 5,
"hashes": [
"1122",
"3344",
"5566",
"7788",
"99AA"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 45 112233445566778899AA 01000000",
"notes": "path_len=0x45: hash_size=2, count=5. Path: 10 bytes."
},
{
"id": "hs-020",
"description": "Hash size 3 (code 2), 1 hop",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 1,
"hashes": [
"AABBCC"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 81 AABBCC 01000000",
"notes": "path_len=0x81: bits 6-7=10 (size code 2, hash_size=3), bits 0-5=1. Path: 3 bytes."
},
{
"id": "hs-021",
"description": "Hash size 3 (code 2), 3 hops",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 3,
"hash_count": 3,
"hashes": [
"AABBCC",
"DDEEFF",
"112233"
]
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 83 AABBCC DDEEFF 112233 01000000",
"notes": "path_len=0x83: hash_size=3, count=3. Path: 9 bytes."
}
]
}
@@ -0,0 +1,183 @@
{
"$schema": "../../schema/test-vector.schema.json",
"name": "Transport Codes - Basic",
"spec_ref": "spec/01-wire-format.md",
"protocol_version": 0,
"description": "Tests transport code encoding with various values and route types that include/exclude transport codes.",
"vectors": [
{
"id": "tc-001",
"description": "Transport codes with TRANSPORT_FLOOD, both zero",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_flood"
},
"transport_codes": [
0,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0C 0000 0000 00 01000000",
"notes": "Both transport codes zero."
},
{
"id": "tc-002",
"description": "Transport codes with TRANSPORT_DIRECT, typical values",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_direct"
},
"transport_codes": [
1000,
2000
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0F E803 D007 00 01000000",
"notes": "TC1: 1000=0x03E8 LE=E803. TC2: 2000=0x07D0 LE=D007."
},
{
"id": "tc-003",
"description": "Transport codes at maximum uint16 values",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_flood"
},
"transport_codes": [
65535,
65535
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0C FFFF FFFF 00 01000000",
"notes": "Both transport codes at maximum: 0xFFFF."
},
{
"id": "tc-004",
"description": "Transport codes: code1=1, code2=0",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_flood"
},
"transport_codes": [
1,
0
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0C 0100 0000 00 01000000",
"notes": "TC1=1 LE=0100, TC2=0."
},
{
"id": "tc-005",
"description": "Transport codes: code1=0x8000, code2=0x0001 (verify high bit handling)",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "transport_direct"
},
"transport_codes": [
32768,
1
],
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0F 0080 0100 00 01000000",
"notes": "TC1: 0x8000 LE=0080. TC2: 1 LE=0100."
},
{
"id": "tc-006",
"description": "FLOOD route (no transport codes) - verify they are absent from wire",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "flood"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0D 00 01000000",
"notes": "FLOOD route: no transport codes on wire. Total: 1 (header) + 1 (path_len) + 4 (ACK) = 6 bytes."
},
{
"id": "tc-007",
"description": "DIRECT route (no transport codes) - verify they are absent from wire",
"type": "encode_decode",
"structured": {
"header": {
"version": 0,
"payload_type": "ack",
"route_type": "direct"
},
"path": {
"hash_size": 1,
"hash_count": 0,
"hashes": []
},
"payload": {
"ack_crc": "00000001"
}
},
"binary": "0E 00 01000000",
"notes": "DIRECT route: no transport codes on wire."
}
]
}
+135
View File
@@ -0,0 +1,135 @@
# MeshCore Protocol Specification
## Section 0: Overview
### Status
This document specifies the MeshCore mesh networking protocol as implemented in
the [reference firmware](https://github.com/meshcore-dev/MeshCore). It is
derived from the source code and official documentation, and serves as the
authoritative definition of the wire format for conformance testing.
### Scope
This specification covers:
- The packet wire format (framing, header, path, payload)
- All payload types and their binary encodings
- Cryptographic operations (encryption, signing, key exchange, hashing)
- The BLE/Serial companion protocol
- The KISS modem protocol
- The bridge protocol (RS232/ESP-NOW)
It does NOT cover:
- Application-level behavior (e.g., room server logic, repeater policies)
- Radio-layer parameters (LoRa spreading factor, bandwidth, frequency)
- Duty cycle management or transmission scheduling
- User interface or companion app behavior
### Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
Additional terms used throughout:
| Term | Definition |
|------|-----------|
| **Packet** | The fundamental transmission unit in MeshCore. |
| **Header** | The first byte of a packet, encoding route type, payload type, and protocol version. |
| **Path** | A sequence of node hashes representing the route a packet has taken or should follow. |
| **Hash** | A truncated prefix of a node's Ed25519 public key, used for routing. |
| **Payload** | The data portion of a packet, whose structure depends on the payload type. |
| **Transport Codes** | Optional 4-byte field present in transport-mode packets, used for regional scoping. |
| **MAC** | Message Authentication Code — a 2-byte truncated HMAC-SHA256 used for integrity verification. |
| **MTU** | Maximum Transmission Unit — 255 bytes for MeshCore. |
| **Flood Routing** | Routing mode where packets are broadcast and repeated by intermediate nodes, building up the path as they travel. |
| **Direct Routing** | Routing mode where the path is supplied by the sender and the packet follows a specific route. |
### Byte Order
All multi-byte integer fields in MeshCore are encoded in **little-endian** byte
order unless explicitly stated otherwise. The one exception is CayenneLPP sensor
data, which uses big-endian encoding.
### Notation
- Binary data is shown as hex bytes separated by spaces: `0D 00 EF BE AD DE`
- Bit fields are shown in binary with MSB first: `0bVVPPPPRR`
- Bit numbering is LSB = bit 0, MSB = bit 7
- Field sizes are in bytes unless noted as bits
- `[field(N)]` denotes a field of N bytes
- `[field(N)?]` denotes an optional field of N bytes
### Protocol Constants
These constants define the fundamental limits of the protocol:
| Constant | Value | Description |
|----------|-------|-------------|
| `MAX_TRANS_UNIT` | 255 | Maximum packet size on the wire (bytes) |
| `MAX_PACKET_PAYLOAD` | 184 | Maximum payload size (bytes) |
| `MAX_PATH_SIZE` | 64 | Maximum path size (bytes) |
| `PUB_KEY_SIZE` | 32 | Ed25519 public key size (bytes) |
| `PRV_KEY_SIZE` | 64 | Ed25519 private key size (bytes) |
| `SIGNATURE_SIZE` | 64 | Ed25519 signature size (bytes) |
| `CIPHER_KEY_SIZE` | 16 | AES-128 key size (bytes) |
| `CIPHER_BLOCK_SIZE` | 16 | AES-128 block size (bytes) |
| `CIPHER_MAC_SIZE` | 2 | Truncated HMAC-SHA256 MAC size (bytes) |
| `PATH_HASH_SIZE` | 1 | Default path hash size for v1 (bytes) |
| `MAX_HASH_SIZE` | 8 | Maximum hash size for deduplication (bytes) |
| `MAX_ADVERT_DATA_SIZE` | 32 | Maximum advertisement app data size (bytes) |
### Protocol Versions
The protocol version is encoded in bits 6-7 of the header byte:
| Version | Value | Status | Description |
|---------|-------|--------|-------------|
| V1 | `0x00` | Active | 1-byte src/dest hashes, 2-byte MAC |
| V2 | `0x01` | Reserved | Future (e.g., 2-byte hashes, 4-byte MAC) |
| V3 | `0x02` | Reserved | Future |
| V4 | `0x03` | Reserved | Future |
Currently only V1 is defined. Implementations MUST support V1.
Implementations SHOULD reject packets with unrecognized version values unless
they have explicit support for that version.
### Document Organization
| Section | Title | Description |
|---------|-------|-------------|
| 00 | Overview | This document |
| 01 | Wire Format | Packet framing and field layout |
| 02 | Header | Header byte encoding |
| 03 | Path | Path length encoding and path field |
| 04 | Payload: ACK | Acknowledgment payload |
| 05 | Payload: Advertisement | Node advertisement and app data |
| 06 | Payload: Encrypted | REQ/RESPONSE/TXT_MSG payloads |
| 07 | Payload: Anonymous Request | Anonymous request payload |
| 08 | Payload: Group | Group text and data payloads |
| 09 | Payload: Path Return | Encrypted path return payload |
| 10 | Payload: Trace | Path trace payload |
| 11 | Payload: Multipart | Multi-packet payload |
| 12 | Payload: Control | Control and discovery payloads |
| 13 | Payload: Raw Custom | Custom raw payload |
| 14 | Cryptography | AES-128, HMAC-SHA256, encrypt-then-MAC |
| 15 | Identity | Ed25519 keys, ECDH, key hashing |
| 16 | Packet Hash | SHA-256 deduplication hashing |
| 17 | Routing | Flood and direct routing behavior |
| 18 | Companion Protocol | BLE/Serial companion communication |
| 19 | KISS Protocol | KISS modem framing and extensions |
| 20 | Bridge Protocol | RS232/ESP-NOW bridge framing |
### References
- [MeshCore Firmware](https://github.com/meshcore-dev/MeshCore) — Reference C++ implementation
- [MeshCore Documentation](https://docs.meshcore.io) — Official protocol documentation
- [MeshCore.js](https://github.com/meshcore-dev/meshcore.js) — JavaScript implementation
- [meshcore-rs](https://github.com/Duncaen/meshcore-rs) — Rust implementation (community)
- [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) — Key words for requirement levels
- [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) — Ed25519 digital signatures
- [FIPS 197](https://csrc.nist.gov/publications/detail/fips/197/final) — AES specification
- [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) — SHA-256 specification
+140
View File
@@ -0,0 +1,140 @@
# 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/`, `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
+125
View File
@@ -0,0 +1,125 @@
# MeshCore Protocol Specification
## Section 2: Header
### Overview
The header is the first byte of every MeshCore packet. It encodes three fields
using bit packing: the route type (2 bits), the payload type (4 bits), and the
protocol version (2 bits).
### Bit Layout
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| V1 V0| P3 P2 P1 P0| R1 R0|
+---+---+---+---+---+---+---+---+
| | |
| | +-- Route Type (bits 0-1)
| +-------------- Payload Type (bits 2-5)
+------------------------ Protocol Version (bits 6-7)
```
The header byte value is computed as:
```
header = (version << 6) | (payload_type << 2) | route_type
```
And fields are extracted as:
```
route_type = header & 0x03
payload_type = (header >> 2) & 0x0F
version = (header >> 6) & 0x03
```
### Route Type (bits 0-1)
The route type determines how the packet is routed through the mesh and whether
transport codes are present.
| Value | Name | Transport Codes | Description |
|-------|------|----------------|-------------|
| 0x00 | TRANSPORT_FLOOD | Yes (4 bytes) | Flood routing with transport codes for regional scoping |
| 0x01 | FLOOD | No | Standard flood routing; path is built up by intermediate nodes |
| 0x02 | DIRECT | No | Direct routing; path is supplied by the sender |
| 0x03 | TRANSPORT_DIRECT | Yes (4 bytes) | Direct routing with transport codes |
Transport codes MUST be present in the wire format when route type is 0x00 or
0x03, and MUST NOT be present when route type is 0x01 or 0x02.
### Payload Type (bits 2-5)
The payload type identifies the structure of the payload data.
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | REQUEST | Encrypted request (dest_hash + src_hash + MAC + ciphertext) |
| 0x01 | RESPONSE | Encrypted response to REQ or ANON_REQ |
| 0x02 | TXT_MSG | Encrypted text message |
| 0x03 | ACK | Simple acknowledgment (4-byte CRC) |
| 0x04 | ADVERT | Node identity advertisement |
| 0x05 | GRP_TXT | Encrypted group text message |
| 0x06 | GRP_DATA | Encrypted group datagram |
| 0x07 | ANON_REQ | Anonymous request (full public key instead of hash) |
| 0x08 | PATH | Encrypted returned path information |
| 0x09 | TRACE | Path trace with per-hop SNR collection |
| 0x0A | MULTIPART | One part of a multi-packet sequence |
| 0x0B | CONTROL | Control/discovery packet |
| 0x0C | (reserved) | Reserved for future use |
| 0x0D | (reserved) | Reserved for future use |
| 0x0E | (reserved) | Reserved for future use |
| 0x0F | RAW_CUSTOM | Custom raw bytes for application-defined payloads |
Implementations SHOULD silently discard packets with reserved payload type values
(0x0C-0x0E) unless they have explicit support for extended types.
### Protocol Version (bits 6-7)
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | V1 | Current version: 1-byte src/dest hashes, 2-byte MAC |
| 0x01 | V2 | Reserved for future use |
| 0x02 | V3 | Reserved for future use |
| 0x03 | V4 | Reserved for future use |
Implementations MUST support version 0x00 (V1). Implementations SHOULD reject
packets with unrecognized version values.
### Special Header Values
The header value `0xFF` is used internally as a "do not retransmit" marker. This
is an in-memory sentinel only and MUST NOT appear on the wire. A header of
`0xFF` would decode as version=3, payload_type=0x0F (RAW_CUSTOM), route_type=3
(TRANSPORT_DIRECT).
### Encoding Examples
| Header Byte | Binary | Version | Payload Type | Route Type |
|-------------|--------|---------|-------------|------------|
| `0x01` | `00 000000 01` | V1 (0) | REQUEST (0) | FLOOD (1) |
| `0x05` | `00 000001 01` | V1 (0) | RESPONSE (1) | FLOOD (1) |
| `0x09` | `00 000010 01` | V1 (0) | TXT_MSG (2) | FLOOD (1) |
| `0x0D` | `00 000011 01` | V1 (0) | ACK (3) | FLOOD (1) |
| `0x11` | `00 000100 01` | V1 (0) | ADVERT (4) | FLOOD (1) |
| `0x0C` | `00 000011 00` | V1 (0) | ACK (3) | TRANSPORT_FLOOD (0) |
| `0x0E` | `00 000011 10` | V1 (0) | ACK (3) | DIRECT (2) |
| `0x0F` | `00 000011 11` | V1 (0) | ACK (3) | TRANSPORT_DIRECT (3) |
| `0x4D` | `01 000011 01` | V2 (1, reserved) | ACK (3) | FLOOD (1) |
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure
- [Section 3: Path](03-path.md) — Path encoding
- Test vectors: `corpus/wire-format/header/`
### Reference Implementation
- `Packet::getRouteType()` in `src/Packet.h` — `header & 0x03`
- `Packet::getPayloadType()` in `src/Packet.h` — `(header >> 2) & 0x0F`
- `Packet::getPayloadVer()` in `src/Packet.h` — `(header >> 6) & 0x03`
- `Packet::hasTransportCodes()` in `src/Packet.h` — route type 0x00 or 0x03
- Route type constants in `src/Packet.h`
- Payload type constants in `src/Packet.h`
+155
View File
@@ -0,0 +1,155 @@
# MeshCore Protocol Specification
## Section 3: Path
### Overview
The path field carries routing information as a sequence of node hashes. In
flood routing, intermediate nodes append their hash to the path as they forward
the packet. In direct routing, the sender supplies the full path. The path
length byte uses bit packing to encode both the number of hashes and the size of
each hash.
### Path Length Byte
The path length byte immediately follows the header (and transport codes, if
present). It encodes two values:
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| S1 S0| C5 C4 C3 C2 C1 C0|
+---+---+---+---+---+---+---+---+
| |
| +-- Hash Count (bits 0-5): 0-63
+----------------- Hash Size Code (bits 6-7): 0-2 (3 is reserved)
```
| Field | Bits | Extraction | Range | Description |
|-------|------|-----------|-------|-------------|
| Hash Count | 0-5 | `path_len & 0x3F` | 0-63 | Number of hashes in the path |
| Hash Size Code | 6-7 | `path_len >> 6` | 0-2 | Encoded hash size: actual size = code + 1 |
The **actual hash size** in bytes is:
```
hash_size = (path_len >> 6) + 1
```
| Hash Size Code | Actual Hash Size | Description |
|---------------|-----------------|-------------|
| 0 (`0b00`) | 1 byte | V1 default (PATH_HASH_SIZE) |
| 1 (`0b01`) | 2 bytes | Extended precision |
| 2 (`0b10`) | 3 bytes | Extended precision |
| 3 (`0b11`) | **RESERVED** | MUST be rejected as invalid |
The path length byte is constructed as:
```
path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F)
```
### Path Field
The path field immediately follows the path length byte. Its size in bytes is:
```
path_byte_length = hash_count × hash_size
```
The path contains `hash_count` consecutive hashes, each `hash_size` bytes long.
Each hash is a prefix of a node's Ed25519 public key (see [Section 15](15-identity.md)).
```
+----------+----------+-----+----------+
| Hash 0 | Hash 1 | ... | Hash N-1 |
| (H bytes)| (H bytes)| ... | (H bytes)|
+----------+----------+-----+----------+
where H = hash_size, N = hash_count
```
When hash_count is 0, the path field is empty (zero bytes on the wire).
### Validation Rules
A conforming implementation MUST reject a packet if any of the following are
true:
1. **Reserved hash size**: Hash size code is 3 (bits 6-7 = `0b11`), meaning
hash_size would be 4. This value is reserved for future use.
2. **Path overflow**: `hash_count × hash_size > MAX_PATH_SIZE` (64 bytes).
The maximum number of hashes depends on hash size:
| Hash Size | Max Hash Count | Max Path Bytes |
|-----------|---------------|----------------|
| 1 byte | 63 | 63 |
| 2 bytes | 32 | 64 |
| 3 bytes | 21 | 63 |
Note: With 1-byte hashes the maximum hash count is 63 (not 64), because the
count field is only 6 bits wide. With 2-byte hashes, 32 × 2 = 64 bytes exactly
fills MAX_PATH_SIZE. With 3-byte hashes, 21 × 3 = 63 bytes is the maximum
before 22 × 3 = 66 would overflow.
### Path Manipulation
**Appending a hash (flood routing):**
When a node forwards a flood-routed packet, it appends its own hash to the path:
1. Let `n` = current hash_count.
2. Verify `(n + 1) × hash_size <= MAX_PATH_SIZE`. If not, the node MUST NOT
forward the packet.
3. Copy the node's hash (first `hash_size` bytes of its public key) to
`path[n × hash_size]`.
4. Increment hash_count: `setPathHashCount(n + 1)`.
**Setting hash size and count:**
```
path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F)
```
### Encoding Examples
| path_len byte | Binary | Hash Size Code | Hash Count | Hash Size | Path Bytes |
|---------------|--------|---------------|------------|-----------|------------|
| `0x00` | `00 000000` | 0 | 0 | 1 | 0 |
| `0x01` | `00 000001` | 0 | 1 | 1 | 1 |
| `0x03` | `00 000011` | 0 | 3 | 1 | 3 |
| `0x3F` | `00 111111` | 0 | 63 | 1 | 63 |
| `0x41` | `01 000001` | 1 | 1 | 2 | 2 |
| `0x42` | `01 000010` | 1 | 2 | 2 | 4 |
| `0x60` | `01 100000` | 1 | 32 | 2 | 64 |
| `0x81` | `10 000001` | 2 | 1 | 3 | 3 |
| `0x95` | `10 010101` | 2 | 21 | 3 | 63 |
| `0xC0` | `11 000000` | 3 | 0 | **INVALID** | — |
| `0xFF` | `11 111111` | 3 | 63 | **INVALID** | — |
### Known Implementation Discrepancy
The Rust implementation [meshcore-rs](https://github.com/Duncaen/meshcore-rs)
treats the path_len byte as a raw byte count rather than decoding hash_count and
hash_size from the bitfields. For example, a path_len value of `0x42` (binary
`01 000010`) should decode as hash_size=2, hash_count=2, path_bytes=4. The
incorrect interpretation reads it as a raw count of 66 bytes.
Test vectors in this corpus specifically exercise multi-byte hash sizes to catch
this class of bug.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure
- [Section 2: Header](02-header.md) — Header encoding
- [Section 15: Identity](15-identity.md) — Hash derivation from public keys
- Test vectors: `corpus/wire-format/path/`
### Reference Implementation
- `Packet::getPathHashSize()` in `src/Packet.h` — `(path_len >> 6) + 1`
- `Packet::getPathHashCount()` in `src/Packet.h` — `path_len & 63`
- `Packet::getPathByteLen()` in `src/Packet.h` — `getPathHashCount() * getPathHashSize()`
- `Packet::setPathHashSizeAndCount()` in `src/Packet.h` — `((sz - 1) << 6) | (n & 63)`
- `Packet::isValidPathLen()` in `src/Packet.cpp` — Validation logic
- `Packet::writePath()` in `src/Packet.cpp` — Path serialization
+57
View File
@@ -0,0 +1,57 @@
# MeshCore Protocol Specification
## Section 4: Payload — ACK
### Overview
The ACK (acknowledgment) payload is the simplest payload type. It consists of a
single 4-byte CRC value used to confirm receipt of a previously sent message.
The CRC is computed by the sender from the original message's timestamp, text,
and sender public key.
### Payload Type
Header payload type field: `0x03` (ACK)
### Wire Format
```
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ACK CRC (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| ack_crc | 0 | 4 bytes | uint32_le | CRC identifying the acknowledged message |
### Encoding
1. Compute the 4-byte CRC value for the message being acknowledged.
2. Write the CRC as a little-endian 32-bit unsigned integer.
3. Set payload_len to 4.
### Decoding
1. Read 4 bytes from the payload as a little-endian uint32.
2. If fewer than 4 bytes are available, the packet is INVALID.
### Constraints
- The payload MUST be exactly 4 bytes.
- Implementations SHOULD log or discard ACK packets with payload_len < 4.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet framing
- [Section 11: Multipart](11-payload-multipart.md) — Multipart ACK encoding
- Test vectors: `corpus/payloads/ack/`
### Reference Implementation
- `Mesh::createAck()` in `src/Mesh.cpp`
- ACK reception in `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ACK`
+123
View File
@@ -0,0 +1,123 @@
# MeshCore Protocol Specification
## Section 5: Payload — Advertisement
### Overview
The advertisement payload allows a node to announce its identity to the mesh. It
contains the node's Ed25519 public key, a timestamp, a signature proving
ownership of the key, and optional application data describing the node's type,
location, features, and name.
### Payload Type
Header payload type field: `0x04` (ADVERT)
### Wire Format
```
0 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ed25519 Public Key (32 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ed25519 Signature (64 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| App Data (0-32 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| pub_key | 0 | 32 bytes | raw | Ed25519 public key of the advertising node |
| timestamp | 32 | 4 bytes | uint32_le | Unix epoch timestamp of advertisement creation |
| signature | 36 | 64 bytes | raw | Ed25519 signature (see Signature section below) |
| app_data | 100 | 0-32 bytes | structured | Optional application data (see App Data section below) |
### Minimum and Maximum Sizes
- Minimum payload: 100 bytes (pub_key + timestamp + signature, no app_data)
- Maximum payload: 132 bytes (100 + MAX_ADVERT_DATA_SIZE of 32)
### Signature
The signature is computed over the concatenation of:
```
message = pub_key(32) || timestamp_le(4) || app_data(0-32)
```
The node signs this message using its Ed25519 private key. Receivers MUST verify
the signature using the public key from the payload. Packets with invalid
signatures MUST be discarded.
### App Data Format
When present (payload_len > 100), app_data is a structured field beginning with
a flags byte:
```
Bit: 7 6 5 4 3 2 1 0
+-------+-------+-------+-------+---+---+---+---+
|has_ |has_ |has_ |has_ | Node Type |
|name |feat2 |feat1 |loc | (4 bits) |
+-------+-------+-------+-------+---+---+---+---+
```
| Field | Offset | Size | Condition | Type | Description |
|-------|--------|------|-----------|------|-------------|
| flags | 0 | 1 byte | Always | uint8 | See bit layout above |
| latitude | 1 | 4 bytes | flags bit 4 set | int32_le | Latitude × 1,000,000 |
| longitude | 5 | 4 bytes | flags bit 4 set | int32_le | Longitude × 1,000,000 |
| feat1 | varies | 2 bytes | flags bit 5 set | uint16_le | Feature field 1 |
| feat2 | varies | 2 bytes | flags bit 6 set | uint16_le | Feature field 2 |
| name | varies | remaining | flags bit 7 set | UTF-8 | Node name (no null terminator) |
Fields appear in the order listed. The offset of each field depends on which
preceding optional fields are present.
### Node Types (flags bits 0-3)
| Value | Name | Description |
|-------|------|-------------|
| 0 | None | Unspecified |
| 1 | Chat | Chat client node |
| 2 | Repeater | Mesh repeater node |
| 3 | Room | Room server node |
| 4 | Sensor | Sensor node |
### Decoding Algorithm
1. Read 32 bytes as pub_key.
2. Read 4 bytes as timestamp (uint32_le).
3. Read 64 bytes as signature.
4. If offset (100) > payload_len, the packet is INVALID (incomplete).
5. If offset (100) == payload_len, there is no app_data. Done.
6. Read remaining bytes as app_data (up to MAX_ADVERT_DATA_SIZE = 32 bytes).
If more than 32 bytes remain, implementations MUST truncate to 32.
7. Construct the signature verification message: pub_key || timestamp_le || app_data.
8. Verify the Ed25519 signature. If invalid, the receiver MUST discard the packet.
9. Parse app_data:
a. Read 1 byte as flags.
b. If flags bit 4 set: read 4 bytes latitude (int32_le), 4 bytes longitude (int32_le).
c. If flags bit 5 set: read 2 bytes feat1 (uint16_le).
d. If flags bit 6 set: read 2 bytes feat2 (uint16_le).
e. If flags bit 7 set: read remaining bytes as UTF-8 name.
### Cross-References
- [Section 15: Identity](15-identity.md) — Ed25519 key management
- [Section 14: Cryptography](14-crypto.md) — Signature operations
- Test vectors: `corpus/payloads/advert/`
### Reference Implementation
- `Mesh::createAdvert()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ADVERT` in `src/Mesh.cpp` — Decoding and verification
+100
View File
@@ -0,0 +1,100 @@
# MeshCore Protocol Specification
## Section 6: Payload — Encrypted (REQ / RESPONSE / TXT_MSG)
### Overview
The REQUEST, RESPONSE, and TXT_MSG payload types share an identical wire format.
They consist of a destination hash, source hash, and an encrypt-then-MAC
ciphertext blob. The three types differ only in their semantic meaning at the
application layer.
### Payload Types
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | REQUEST | Encrypted request (e.g., login, data query) |
| 0x01 | RESPONSE | Encrypted response to a REQUEST or ANON_REQ |
| 0x02 | TXT_MSG | Encrypted text message |
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| dest_hash | 0 | PATH_HASH_SIZE (1) | raw | First byte of recipient's Ed25519 public key |
| src_hash | 1 | PATH_HASH_SIZE (1) | raw | First byte of sender's Ed25519 public key |
| cipher_mac | 2 | CIPHER_MAC_SIZE (2) | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 4 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 4 + 16 = 20 bytes (dest_hash + src_hash + MAC + 1 AES block)
### Encryption
The ciphertext is produced using the encrypt-then-MAC scheme described in
[Section 14](14-crypto.md):
1. Compute the ECDH shared secret between sender and recipient.
2. Encrypt the plaintext using AES-128-ECB with the first 16 bytes of the
shared secret as the key. Zero-pad the final block.
3. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared secret
as the HMAC key. Truncate to 2 bytes.
4. Prepend the 2-byte MAC to the ciphertext.
### Decryption
1. Extract dest_hash and src_hash.
2. If dest_hash matches this node, search for peers matching src_hash.
3. For each matching peer, compute or retrieve the shared secret.
4. Verify the MAC: compute HMAC-SHA256 over the ciphertext portion using the
full 32-byte shared secret. Compare the first 2 bytes with cipher_mac.
5. If the MAC is valid, decrypt the ciphertext using AES-128-ECB with the first
16 bytes of the shared secret. The decrypted data may contain trailing zero
bytes from padding.
6. If no peer's MAC matches, the packet is not for this node (or the peer is
unknown). The packet MAY still be forwarded.
### Plaintext Format (TXT_MSG)
For TXT_MSG payloads, the decrypted plaintext has this structure:
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| timestamp | 0 | 4 bytes | uint32_le | Message timestamp |
| txt_type_attempt | 4 | 1 byte | uint8 | Upper 6 bits: message type, lower 2 bits: attempt (0-3) |
| text | 5 | remaining | UTF-8 | Message text |
### Plaintext Format (REQUEST / RESPONSE)
The plaintext format for REQUEST and RESPONSE is application-defined. Common
patterns include:
- **REQUEST**: `[timestamp(4)][request_data...]`
- **RESPONSE**: `[timestamp(4)][response_data...]`
### Cross-References
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC details
- [Section 15: Identity](15-identity.md) — ECDH shared secret computation
- Test vectors: `corpus/payloads/encrypted/`
### Reference Implementation
- `Mesh::createDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_REQ/RESPONSE/TXT_MSG` — Decoding
- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encryption
- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — Decryption and verification
+74
View File
@@ -0,0 +1,74 @@
# MeshCore Protocol Specification
## Section 7: Payload — Anonymous Request
### Overview
The anonymous request payload allows a node to send an encrypted message to a
recipient without the recipient needing to know the sender in advance. Instead of
a 1-byte source hash, the full 32-byte Ed25519 public key of the sender is
included, enabling the recipient to compute the shared secret for decryption.
### Payload Type
Header payload type field: `0x07` (ANON_REQ)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | |
+-+-+-+-+-+-+-+-+ |
| Sender Public Key (32 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Cipher MAC (2 bytes) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| dest_hash | 0 | 1 byte | raw | First byte of recipient's Ed25519 public key |
| sender_pub_key | 1 | 32 bytes | raw | Full Ed25519 public key of sender |
| cipher_mac | 33 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 35 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 1 + 32 + 2 + 16 = 51 bytes
### Encryption
1. Sender computes ECDH shared secret using its private key and the recipient's
public key.
2. Encrypt plaintext using AES-128-ECB with first 16 bytes of shared secret.
3. Compute HMAC-SHA256 over ciphertext with full 32-byte shared secret.
Truncate to 2 bytes.
4. Assemble: dest_hash || sender_pub_key || MAC || ciphertext.
### Decryption
1. Read dest_hash. If it does not match this node, the packet is not for us
(but MAY be forwarded).
2. Read 32-byte sender_pub_key. Construct an Identity from it.
3. Compute ECDH shared secret between this node's private key and sender's
public key.
4. Verify the MAC and decrypt as in [Section 14](14-crypto.md).
### Cross-References
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Standard encrypted format
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- [Section 15: Identity](15-identity.md) — ECDH key exchange
- Test vectors: `corpus/payloads/anon-req/`
### Reference Implementation
- `Mesh::createAnonDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ANON_REQ` — Decoding
+81
View File
@@ -0,0 +1,81 @@
# MeshCore Protocol Specification
## Section 8: Payload — Group (GRP_TXT / GRP_DATA)
### Overview
Group payloads enable encrypted communication within a channel. All members of a
channel share a symmetric secret. The payload begins with a 1-byte channel hash
(derived from the channel's shared key), followed by an encrypt-then-MAC blob.
### Payload Types
| Value | Name | Description |
|-------|------|-------------|
| 0x05 | GRP_TXT | Group text message |
| 0x06 | GRP_DATA | Group datagram (binary data) |
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|channel_hash(1)| Cipher MAC (2 bytes) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| channel_hash | 0 | 1 byte | raw | First byte of SHA-256(channel_shared_key) |
| cipher_mac | 1 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 3 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 1 + 2 + 16 = 19 bytes
### Channel Hash Derivation
The channel_hash is the first byte of the SHA-256 hash of the channel's 32-byte
shared key (PUB_KEY_SIZE bytes):
```
channel_hash = SHA256(channel_shared_key)[0]
```
This means hash collisions are expected (1 in 256 chance). When a receiver finds
a matching channel_hash, it attempts decryption with each matching channel's
secret. Only a successful MAC verification confirms the correct channel.
### Encryption
1. Encrypt plaintext using AES-128-ECB with first 16 bytes of the channel's
shared key (GroupChannel.secret).
2. Compute HMAC-SHA256 over ciphertext using all 32 bytes of the channel's
shared key. Truncate to 2 bytes.
3. Assemble: channel_hash || MAC || ciphertext.
### Decryption
1. Read channel_hash (1 byte).
2. Search local channel database for channels with matching hash. Multiple
channels may match (up to 4 in reference implementation).
3. For each matching channel, attempt MAC verification and decryption using the
channel's shared key.
4. The first channel that produces a valid MAC yields the correct decryption.
### Cross-References
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- Test vectors: `corpus/payloads/group/`
### Reference Implementation
- `Mesh::createGroupDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_GRP_TXT/GRP_DATA` — Decoding
- `Mesh::searchChannelsByHash()` — Channel lookup
+90
View File
@@ -0,0 +1,90 @@
# MeshCore Protocol Specification
## Section 9: Payload — Path Return
### Overview
The PATH payload carries an encrypted return path from one node to another. It
uses the same dest_hash + src_hash + encrypt-then-MAC envelope as REQ/RESPONSE/
TXT_MSG, but the decrypted contents contain routing path data and optional extra
application data.
### Payload Type
Header payload type field: `0x08` (PATH)
### Wire Format (Outer)
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The outer format is identical to [Section 6](06-payload-encrypted.md).
### Decrypted Inner Format
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| path_len(1) | Path (hash_count × hash_size) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| extra_type(1) | Extra Data (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
| Field | Size | Type | Description |
|-------|------|------|-------------|
| path_len | 1 byte | encoded | Same bitfield encoding as packet path_len (see [Section 3](03-path.md)) |
| path | hash_count × hash_size | raw | The return path to the sender |
| extra_type | 1 byte | uint8 | Type of extra data (lower 4 bits used; upper 4 reserved) |
| extra | remaining | raw | Extra data (may include trailing zero padding from AES) |
### Extra Type Values
| Value | Description |
|-------|-------------|
| 0x00 | No meaningful extra data (reciprocal path) |
| 0xFF | Dummy/padding (used when no real extra data; followed by 4 random bytes) |
| Other | Application-defined |
When creating a path return with no extra data, the reference implementation
appends `0xFF` as extra_type followed by 4 random bytes to ensure the packet
hash is unique.
### Encoding
1. Build the inner plaintext:
a. Write path_len byte (encoded hash_size and hash_count).
b. Write path bytes (hash_count × hash_size).
c. If extra data is provided: write extra_type (lower 4 bits), write extra.
d. If no extra data: write `0xFF`, write 4 random bytes.
2. Encrypt the inner plaintext using encrypt-then-MAC with the shared secret.
3. Prepend dest_hash and src_hash.
### Decoding
1. Decode the outer envelope (dest_hash, src_hash, MAC, ciphertext) as in
[Section 6](06-payload-encrypted.md).
2. After decryption:
a. Read 1 byte as path_len. Decode hash_size and hash_count.
b. Read hash_count × hash_size bytes as the path.
c. Read 1 byte as extra_type (use lower 4 bits only).
d. Remaining bytes are extra data (may include AES zero-padding).
### Cross-References
- [Section 3: Path](03-path.md) — Path length encoding
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Outer envelope format
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- Test vectors: `corpus/payloads/path-return/`
### Reference Implementation
- `Mesh::createPathReturn()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, PATH case within REQ/RESPONSE/TXT_MSG handling — Decoding
+106
View File
@@ -0,0 +1,106 @@
# MeshCore Protocol Specification
## Section 10: Payload — Trace
### Overview
The TRACE payload enables path discovery with per-hop signal quality measurement.
A trace packet travels along a specified direct path, and each intermediate node
appends its received SNR value to the packet's path field. When the trace reaches
its final hop, the accumulated SNR values and node hashes are delivered.
### Payload Type
Header payload type field: `0x09` (TRACE)
### Wire Format
The trace payload has a fixed 9-byte header followed by an optional path hashes
section (appended when sent via direct routing):
```
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Tag (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Auth Code (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags | Path Hashes (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| tag | 0 | 4 bytes | uint32_le | Random tag set by the initiator for correlation |
| auth_code | 4 | 4 bytes | uint32_le | Authentication code |
| flags | 8 | 1 byte | uint8 | Flags (see below) |
| path_hashes | 9 | variable | raw | Node hashes for the path to trace (appended for direct send) |
### Flags Field
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Reserved | S1 S0|
+---+---+---+---+---+---+---+---+
```
| Bits | Description |
|------|-------------|
| 0-1 | Path hash size for the trace path hashes (as a power of 2: 0=1 byte, 1=2 bytes, 2=4 bytes) |
| 2-7 | Reserved (must be zero) |
Note: The flags field path hash size encoding (`1 << (flags & 0x03)`) differs
from the packet path_len encoding (`(code >> 6) + 1`).
### Direct Sending
When a TRACE packet is sent via direct routing, the path hashes are appended
directly to the payload (after the 9-byte header), not placed in the packet's
path field. The packet's path field is instead used to accumulate SNR values
from intermediate nodes.
```
On creation: payload = [tag(4)][auth_code(4)][flags(1)] (9 bytes)
On sendDirect: payload += path_hashes (9 + N bytes)
packet.path_len = 0 (path used for SNR)
```
### SNR Accumulation
As each intermediate node forwards a TRACE packet, it appends its received SNR
value as a single signed byte to the packet's path field:
```
path[path_len++] = (int8_t)(SNR × 4)
```
The SNR value is stored as SNR × 4 for 0.25 dB precision, matching the format
used in RxMeta frames.
### Trace Completion
A TRACE is considered complete when the path_len counter (used as an offset into
the path hashes in the payload) reaches or exceeds the remaining path hashes.
At that point, `onTraceRecv` is called with:
- The accumulated SNR values (in packet.path)
- The path hashes (in packet.payload, after byte 9)
- The path length (number of hops traversed)
### Minimum Size
- Minimum payload: 9 bytes (tag + auth_code + flags, no path hashes)
### Cross-References
- [Section 16: Packet Hash](16-packet-hash.md) — TRACE packets include path_len in hash
- Test vectors: `corpus/payloads/trace/`
### Reference Implementation
- `Mesh::createTrace()` in `src/Mesh.cpp` — Creation (9-byte base)
- `Mesh::sendDirect()` — Path appended to payload for TRACE type
- `Mesh::onRecvPacket()`, TRACE handling — SNR accumulation and completion check
+88
View File
@@ -0,0 +1,88 @@
# MeshCore Protocol Specification
## Section 11: Payload — Multipart
### Overview
The MULTIPART payload wraps another payload type to indicate it is part of a
multi-packet sequence. The first byte encodes the number of remaining packets in
the sequence and the sub-payload type. Currently, only multipart ACK is defined
in the reference implementation.
### Payload Type
Header payload type field: `0x0A` (MULTIPART)
### Wire Format
```
0 1
0 1 2 3 4 5 6 7 8 9 0 ...
+-+-+-+-+-+-+-+-+-+-+-+-+
|Rem| SubType | Sub-payload |
+-+-+-+-+-+-+-+-+-+-+-+-+
```
First byte encoding:
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Remaining (4) | Sub-Type (4) |
+---+---+---+---+---+---+---+---+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| remaining | 0 (bits 4-7) | 4 bits | uint | Number of remaining packets in sequence (0-15) |
| sub_type | 0 (bits 0-3) | 4 bits | uint | Payload type of the wrapped content |
| sub_payload | 1 | variable | raw | The wrapped payload data |
```
first_byte = (remaining << 4) | (sub_type & 0x0F)
```
### Multipart ACK
The only currently defined multipart sub-type is ACK (sub_type = 0x03):
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (rem<<4)|0x03| ACK CRC (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
Total payload: 5 bytes (1 byte header + 4 byte CRC).
Multipart ACKs are used to send additional ACK retransmissions for reliability.
The `remaining` field indicates how many more ACK packets follow this one.
### Encoding (Multipart ACK)
1. Set first byte to `(remaining << 4) | PAYLOAD_TYPE_ACK`.
2. Copy the 4-byte ACK CRC starting at offset 1.
3. Set payload_len to 5.
### Decoding (Multipart ACK)
1. Read first byte. Extract remaining = byte >> 4, sub_type = byte & 0x0F.
2. If sub_type == 0x03 (ACK) and payload_len >= 5:
a. Read 4 bytes at offset 1 as ACK CRC (uint32_le).
3. For other sub_types: reserved for future use.
### Constraints
- Payload MUST be at least 2 bytes (1 header + 1 sub-payload minimum).
- For multipart ACK, payload MUST be at least 5 bytes.
### Cross-References
- [Section 4: ACK](04-payload-ack.md) — ACK CRC format
- Test vectors: `corpus/payloads/multipart/`
### Reference Implementation
- `Mesh::createMultiAck()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_MULTIPART` — Decoding
+68
View File
@@ -0,0 +1,68 @@
# MeshCore Protocol Specification
## Section 12: Payload — Control
### Overview
The CONTROL payload carries control and discovery data. Control packets are raw
byte payloads whose structure is determined by the first byte. A subset of
control packets (those with bit 7 of the first byte set) are restricted to
zero-hop delivery only.
### Payload Type
Header payload type field: `0x0B` (CONTROL)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Control Byte | Data... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| control_byte | 0 | 1 byte | uint8 | Control type identifier |
| data | 1 | variable | raw | Control-specific data |
### Zero-Hop Restriction
When bit 7 (`0x80`) of the control byte is set, the packet is restricted to
zero-hop delivery only. The reference implementation enforces this:
- If `(payload[0] & 0x80) != 0` AND the packet is direct-routed:
- It is processed only if `getPathHashCount() == 0`.
- It is NOT forwarded to other nodes.
### Discovery Protocols
Control packets are commonly used for node discovery. The specific sub-protocol
formats are application-defined, but typically include:
- **Discovery Request**: Sent zero-hop to find nearby nodes
- **Discovery Response**: Contains node type, name, SNR, and other metadata
### Encoding
1. Construct the raw control data bytes.
2. If the control packet should be zero-hop only, set bit 7 of the first byte.
3. Copy data to payload, set payload_len.
### Constraints
- Payload MUST be at least 1 byte (the control byte).
- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD).
### Cross-References
- [Section 17: Routing](17-routing.md) — Zero-hop delivery
- Test vectors: `corpus/payloads/control/`
### Reference Implementation
- `Mesh::createControlData()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, CONTROL handling — Zero-hop check and delivery
+49
View File
@@ -0,0 +1,49 @@
# MeshCore Protocol Specification
## Section 13: Payload — Raw Custom
### Overview
The RAW_CUSTOM payload carries application-defined raw bytes with no prescribed
structure. It is intended for applications that implement their own encryption,
framing, or payload formats on top of the MeshCore transport.
### Payload Type
Header payload type field: `0x0F` (RAW_CUSTOM)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Raw Data (1-184 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| data | 0 | 1-184 bytes | raw | Application-defined payload |
### Routing
In the current reference implementation, RAW_CUSTOM packets are only processed
when received via direct routing. They are NOT flood-routed.
### Constraints
- Payload MUST be at least 1 byte.
- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD).
- Maximum total data: limited by `sizeof(Packet::payload)` which is
MAX_PACKET_PAYLOAD (184).
### Cross-References
- Test vectors: `corpus/payloads/raw-custom/`
### Reference Implementation
- `Mesh::createRawData()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_RAW_CUSTOM` — Processing (direct only)
+120
View File
@@ -0,0 +1,120 @@
# MeshCore Protocol Specification
## Section 14: Cryptography
### Overview
MeshCore uses a combination of symmetric and asymmetric cryptography. Data
confidentiality is provided by AES-128 in ECB mode with zero padding. Data
integrity is provided by HMAC-SHA256 truncated to 2 bytes. These are combined
in an encrypt-then-MAC scheme.
### Algorithms
| Operation | Algorithm | Key Size | Output |
|-----------|-----------|----------|--------|
| Symmetric Encryption | AES-128-ECB | 16 bytes | Multiple of 16 bytes |
| Message Authentication | HMAC-SHA256 | 32 bytes | 2 bytes (truncated) |
| Digital Signature | Ed25519 | 32/64 bytes | 64 bytes |
| Key Exchange | X25519 (ECDH) | 32 bytes | 32 bytes |
| Hashing | SHA-256 | — | Up to 32 bytes |
### AES-128-ECB Encryption
MeshCore uses AES-128 in Electronic Codebook (ECB) mode with zero-byte padding.
**Key**: The first 16 bytes (CIPHER_KEY_SIZE) of the shared secret.
**Encryption**:
1. Set the AES-128 key to `shared_secret[0..15]`.
2. Process the plaintext in 16-byte blocks:
a. For each complete 16-byte block, encrypt in place.
b. For the final partial block (< 16 bytes):
- Copy the remaining bytes to a 16-byte buffer.
- Fill remaining bytes with zeros.
- Encrypt the padded block.
3. If the plaintext is empty, encrypt a 16-byte all-zero block.
4. The output is always a multiple of 16 bytes.
**Decryption**:
1. Set the AES-128 key to `shared_secret[0..15]`.
2. Process the ciphertext in 16-byte blocks, decrypting each.
3. The output length equals the ciphertext length.
4. The final block MAY contain trailing zero bytes from padding.
The application must know the original plaintext length or use
a length-prefixed format to determine where meaningful data ends.
### HMAC-SHA256 (Truncated to 2 Bytes)
MeshCore computes HMAC-SHA256 over the ciphertext and truncates the result to
CIPHER_MAC_SIZE (2) bytes.
**Key**: The full 32 bytes (PUB_KEY_SIZE) of the shared secret.
Note: The HMAC key uses the full 32-byte shared secret, while the AES key uses
only the first 16 bytes. This is an important distinction.
**Computation**:
```
mac = HMAC-SHA256(key=shared_secret[0..31], message=ciphertext)[0..1]
```
The first 2 bytes of the HMAC-SHA256 output are used as the MAC.
### Encrypt-then-MAC
The encrypt-then-MAC scheme combines AES-128-ECB encryption with HMAC-SHA256
authentication:
**Encoding** (`encryptThenMAC`):
1. Encrypt the plaintext using AES-128-ECB (see above).
Store ciphertext at `dest[2..]` (offset by MAC size).
2. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared
secret. Truncate to 2 bytes.
3. Store the 2-byte MAC at `dest[0..1]`.
4. Return total length: 2 + ciphertext_length.
Output format:
```
[MAC (2 bytes)][Ciphertext (N × 16 bytes)]
```
**Decoding** (`MACThenDecrypt`):
1. If input length ≤ 2 (CIPHER_MAC_SIZE), return failure (invalid).
2. Recompute HMAC-SHA256 over `src[2..]` (the ciphertext portion) using the
full 32-byte shared secret. Truncate to 2 bytes.
3. Compare the computed MAC with `src[0..1]`.
4. If MACs do not match, return failure (0 = invalid HMAC).
5. If MACs match, decrypt `src[2..]` using AES-128-ECB.
6. Return the decrypted plaintext length.
### Security Notes
- **ECB mode**: AES-ECB encrypts each 16-byte block independently. Identical
plaintext blocks produce identical ciphertext blocks. This is a known
weakness of ECB mode, but MeshCore's short payloads and mesh-network context
make this acceptable for its use case.
- **2-byte MAC**: The truncated HMAC provides only 16 bits of authentication.
This means there is approximately a 1-in-65536 chance of a forged MAC being
accepted. This trade-off prioritizes bandwidth over authentication strength.
- **Key derivation**: The shared secret is used directly as both AES key
(first 16 bytes) and HMAC key (full 32 bytes). No key derivation function
(KDF) is applied.
### Cross-References
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Usage in packet payloads
- [Section 15: Identity](15-identity.md) — Shared secret computation via ECDH
- Test vectors: `corpus/crypto/`
### Reference Implementation
- `Utils::encrypt()` in `src/Utils.cpp` — AES-128-ECB encryption
- `Utils::decrypt()` in `src/Utils.cpp` — AES-128-ECB decryption
- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encrypt-then-MAC
- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — MAC-then-Decrypt
+92
View File
@@ -0,0 +1,92 @@
# MeshCore Protocol Specification
## Section 15: Identity
### Overview
Every MeshCore node has an Ed25519 key pair. The 32-byte public key serves as
the node's identity. Hashes used in routing are truncated prefixes of the public
key. Shared secrets for encryption are computed via ECDH key exchange using
X25519 (Ed25519 keys transposed to Curve25519).
### Key Types
| Type | Size | Description |
|------|------|-------------|
| Public Key | 32 bytes (PUB_KEY_SIZE) | Ed25519 public key |
| Private Key | 64 bytes (PRV_KEY_SIZE) | Ed25519 private key (seed + public) |
| Signature | 64 bytes (SIGNATURE_SIZE) | Ed25519 signature |
| Shared Secret | 32 bytes (PUB_KEY_SIZE) | ECDH shared secret |
### Hash Derivation
A node's routing hash is simply a prefix of its public key:
```
hash = pub_key[0..hash_size-1]
```
Where `hash_size` is 1, 2, or 3 bytes depending on the protocol version and
path encoding (see [Section 3](03-path.md)).
For V1, `PATH_HASH_SIZE` is 1, meaning a node's hash is the first byte of its
public key. This gives only 256 unique hash values, so hash collisions are
common and expected. The protocol handles this by attempting decryption with all
peers matching a given hash.
### Hash Matching
To check if a hash matches a node's identity:
```
match = memcmp(hash, pub_key, hash_size) == 0
```
### Ed25519 Signing
Used for advertisement signatures (see [Section 5](05-payload-advert.md)):
1. Construct the message to sign (e.g., pub_key || timestamp || app_data).
2. Sign using the Ed25519 private key.
3. The 64-byte signature is included in the advertisement payload.
Verification:
1. Extract the public key and signature from the payload.
2. Reconstruct the message.
3. Verify the signature using Ed25519 verification.
4. Receivers MUST discard the packet if verification fails.
### ECDH Key Exchange (X25519)
To compute a shared secret between two nodes:
1. Convert the Ed25519 keys to X25519 (Curve25519) format.
2. Perform X25519 Diffie-Hellman: `shared_secret = X25519(my_private, their_public)`.
3. The resulting 32-byte shared secret is used for:
- AES-128 key: `shared_secret[0..15]`
- HMAC-SHA256 key: `shared_secret[0..31]`
### Identity Serialization
Identities can be serialized for storage:
- **Public Identity**: 32 bytes (public key only)
- **Local Identity**: 64 bytes (private key) + 32 bytes (public key) = 96 bytes
Hex representation uses uppercase characters with 2 hex digits per byte.
### Cross-References
- [Section 3: Path](03-path.md) — Hash size in path encoding
- [Section 5: Advertisement](05-payload-advert.md) — Signature usage
- [Section 14: Cryptography](14-crypto.md) — Shared secret usage
- Test vectors: `corpus/crypto/ed25519/`, `corpus/crypto/ecdh/`
### Reference Implementation
- `Identity` class in `src/Identity.h` — Public key and verification
- `LocalIdentity` class in `src/Identity.h` — Key pair, signing, ECDH
- `Identity::copyHashTo()` — Hash derivation (prefix copy)
- `Identity::isHashMatch()` — Hash comparison
- `LocalIdentity::calcSharedSecret()` — ECDH key exchange
+67
View File
@@ -0,0 +1,67 @@
# MeshCore Protocol Specification
## Section 16: Packet Hash
### Overview
MeshCore uses SHA-256 hashing for packet deduplication. Each node maintains a
table of recently seen packet hashes and discards packets it has already
processed. The hash is computed over the payload type and payload contents, with
a special case for TRACE packets.
### Hash Computation
The packet hash is computed as:
```
hash = SHA-256(payload_type_byte || payload_data)[0..MAX_HASH_SIZE-1]
```
Where:
- `payload_type_byte` is a single byte: `(header >> 2) & 0x0F`
- `payload_data` is the raw payload bytes (payload_len bytes)
- The SHA-256 output is truncated to MAX_HASH_SIZE (8) bytes
### TRACE Packet Special Case
For TRACE packets (payload_type = 0x09), the path_len byte is included in the
hash to distinguish trace packets that revisit the same node on their return
path:
```
hash = SHA-256(payload_type_byte || path_len_byte || payload_data)[0..MAX_HASH_SIZE-1]
```
### Algorithm
```
function calculatePacketHash(packet):
sha = SHA256_init()
type_byte = getPayloadType(packet.header) // single byte
SHA256_update(sha, type_byte)
if type_byte == 0x09: // TRACE
SHA256_update(sha, packet.path_len) // 1 byte (as uint8)
SHA256_update(sha, packet.payload, packet.payload_len)
hash = SHA256_finalize(sha)
return hash[0..7] // first MAX_HASH_SIZE bytes
```
### Properties
- The hash does NOT include the header byte, path, or transport codes. This
means the same logical message received via different routes produces the
same hash.
- The hash does NOT include the route type or version. Only the payload type
and payload content determine the hash.
- For TRACE packets, including path_len ensures that the same trace payload
at different stages of traversal produces different hashes.
### Cross-References
- [Section 10: Trace](10-payload-trace.md) — TRACE packet special handling
- Test vectors: `corpus/crypto/sha256/packet-hash.json`
### Reference Implementation
- `Packet::calculatePacketHash()` in `src/Packet.cpp`
- `MeshTables::hasSeen()` — Deduplication table lookup
+124
View File
@@ -0,0 +1,124 @@
# MeshCore Protocol Specification
## Section 17: Routing
### Overview
MeshCore supports two routing modes: flood routing and direct routing. Flood
routing broadcasts packets to all neighbors, building up a path as the packet
propagates. Direct routing sends packets along a pre-determined path.
### Flood Routing
In flood routing, the sender emits a packet with an empty path. Each
intermediate node that forwards the packet appends its hash to the path before
retransmitting.
**Sending (flood)**:
1. Set route type to FLOOD (0x01) or TRANSPORT_FLOOD (0x00).
2. Set path to empty: `setPathHashSizeAndCount(hash_size, 0)`.
3. Transmit the packet.
**Forwarding (flood)**:
1. Receive a flood-routed packet.
2. Check the deduplication table. If already seen, discard.
3. Process the packet (decrypt if addressed to this node, etc.).
4. If the packet should be forwarded (node is a repeater):
a. Verify `(hash_count + 1) × hash_size <= MAX_PATH_SIZE`.
b. Append this node's hash at `path[hash_count × hash_size]`.
c. Increment hash_count.
d. Retransmit with a random delay.
**Path growth**: The path grows by one hash per hop. When the path reaches
MAX_PATH_SIZE (64 bytes), no more nodes can be appended and the packet stops
propagating.
### Direct Routing
In direct routing, the sender provides the complete path. Each intermediate
node checks whether it is the next hop, removes itself from the path, and
forwards to the next node.
**Sending (direct)**:
1. Set route type to DIRECT (0x02) or TRANSPORT_DIRECT (0x03).
2. Set the path to the destination's known route.
3. Transmit the packet.
**Forwarding (direct)**:
1. Receive a direct-routed packet.
2. Check if the first hash in the path matches this node.
3. If yes, and the node allows forwarding:
a. Remove the first hash from the path (shift remaining hashes left).
b. Decrement hash_count.
c. Retransmit.
4. If no, discard (this node is not the next hop).
**Path removal** (`removeSelfFromPath`):
1. Decrement hash_count.
2. Shift the path array: for each index k from 0 to (hash_count × hash_size),
copy `path[k + hash_size]` to `path[k]`.
### Zero-Hop Delivery
Zero-hop packets are direct-routed packets with hash_count = 0. They reach only
immediate neighbors (nodes within radio range). Used for:
- Control packets with bit 7 set in the control byte
- Discovery protocols
- Local-only operations
### Transport Codes
Transport codes (present in TRANSPORT_FLOOD and TRANSPORT_DIRECT) enable
regional scoping. Nodes can filter packets based on transport codes, accepting
only packets from their region.
### TRACE Routing
TRACE packets are a special case. They are sent via direct routing, but instead
of the normal path field, the path hashes are appended to the payload. The
packet's path field is repurposed to accumulate SNR values from each hop
(see [Section 10](10-payload-trace.md)).
### Retransmission Priority
Packets are prioritized for transmission:
| Priority | Packet Type | Description |
|----------|-------------|-------------|
| 0 (highest) | Direct routed | Routed traffic |
| 1 | Path return, standard | Most flood packets |
| 2 | Path return (flood) | Path packets |
| 3 | Advertisement | De-prioritized |
| 5 | Trace | Trace forwarding |
| N (hash_count) | Flood forwarded | Lower priority for more distant sources |
### Packet Deduplication
Before processing or forwarding any packet, nodes check if the packet has
already been seen using `hasSeen()` with the packet hash from
[Section 16](16-packet-hash.md). This prevents infinite loops in flood routing.
Once a packet is marked as seen, it is NOT forwarded again even if received via
a different route. This is a "first packet wins" approach.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet structure
- [Section 3: Path](03-path.md) — Path encoding
- [Section 10: Trace](10-payload-trace.md) — TRACE routing special case
- [Section 16: Packet Hash](16-packet-hash.md) — Deduplication
### Reference Implementation
- `Mesh::sendFlood()` in `src/Mesh.cpp` — Flood send
- `Mesh::sendDirect()` in `src/Mesh.cpp` — Direct send
- `Mesh::sendZeroHop()` in `src/Mesh.cpp` — Zero-hop send
- `Mesh::routeRecvPacket()` in `src/Mesh.cpp` — Flood forwarding
- `Mesh::removeSelfFromPath()` in `src/Mesh.cpp` — Path manipulation
- `Mesh::onRecvPacket()` in `src/Mesh.cpp` — Direct forwarding
+96
View File
@@ -0,0 +1,96 @@
# MeshCore Protocol Specification
## Section 18: Companion Protocol
### Overview
The companion protocol enables BLE (Bluetooth Low Energy) and serial
communication between a MeshCore radio and a companion app (phone, tablet, or
computer). It uses a binary framing format over the Nordic UART Service (NUS)
BLE profile.
### BLE Service
| UUID | Description |
|------|-------------|
| `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` | Nordic UART Service |
| `6E400002-...` | RX Characteristic (app writes to radio) |
| `6E400003-...` | TX Characteristic (radio notifies app) |
### Frame Format
Each companion protocol frame consists of a 1-byte type identifier followed by
variable-length data:
```
[type(1)][data(0-171)]
```
Maximum frame size is 172 bytes.
### Message Types (Radio to App)
| Type | Value | Description |
|------|-------|-------------|
| PACKET_CHANNEL_MSG_RECV | 0x01 | Channel message received |
| PACKET_CONTACT_MSG_RECV | 0x02 | Contact message received |
| PACKET_ADV_RECV | 0x03 | Advertisement received |
| PACKET_CONTACT_MSG_RECV_V3 | 0x04 | Contact message V3 (includes SNR) |
| PACKET_CHANNEL_MSG_RECV_V3 | 0x05 | Channel message V3 (includes SNR) |
### Command Types (App to Radio)
| Command | Value | Description |
|---------|-------|-------------|
| CMD_APP_START | 0x01 | Initialize connection |
| CMD_DEVICE_QUERY | 0x02 | Query device info |
| CMD_SET_CHANNEL | 0x03 | Set active channel |
| CMD_SEND_CHANNEL_MESSAGE | 0x04 | Send message to channel |
| CMD_SEND_CONTACT_MESSAGE | 0x05 | Send message to contact |
| CMD_GET_CONTACTS | 0x06 | Get contact list |
| CMD_GET_CHANNELS | 0x07 | Get channel list |
| CMD_SET_TIME | 0x08 | Set device time |
| CMD_ADD_CONTACT | 0x09 | Add a contact |
| CMD_ADD_CHANNEL | 0x0A | Add a channel |
| CMD_SEND_LOGIN | 0x0B | Login to room/repeater |
| CMD_GET_SETTINGS | 0x0C | Get device settings |
| CMD_SET_SETTINGS | 0x0D | Set device settings |
| CMD_REMOVE_CONTACT | 0x0E | Remove a contact |
| CMD_REMOVE_CHANNEL | 0x0F | Remove a channel |
| CMD_SHARE_CONTACT | 0x10 | Share contact info |
| CMD_SET_ADV_NAME | 0x11 | Set advertisement name |
| CMD_REBOOT | 0x12 | Reboot device |
| CMD_SEND_RAW | 0x13 | Send raw packet |
### Connection Sequence
1. Scan for BLE devices advertising the Nordic UART Service.
2. Connect and discover services.
3. Enable notifications on the TX characteristic.
4. Send `CMD_APP_START` to initialize.
5. Send `CMD_DEVICE_QUERY` to get device info.
6. Send `CMD_SET_TIME` to synchronize clock.
7. Send `CMD_GET_CONTACTS` and `CMD_GET_CHANNELS` to fetch state.
### Byte Order
All multi-byte integers in the companion protocol are **little-endian**, except
for CayenneLPP sensor data which uses **big-endian**.
### Message Length Limit
Text messages are limited to 133 characters.
### MTU Considerations
The default BLE MTU is 23 bytes (20 bytes of payload). For complex operations,
implementations SHOULD request a larger MTU (up to 512 bytes).
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Underlying packet format
- Test vectors: `corpus/companion/`
### Reference Implementation
- `docs/companion_protocol.md` in the MeshCore repository
+180
View File
@@ -0,0 +1,180 @@
# MeshCore Protocol Specification
## Section 19: KISS Modem Protocol
### Overview
The KISS (Keep It Simple, Stupid) modem protocol provides a serial interface to
MeshCore LoRa radios. It follows the KA9Q/K3MC KISS TNC specification with
MeshCore-specific SetHardware extensions for cryptographic operations, radio
control, and telemetry.
### Serial Configuration
- Baud rate: 115200
- Data bits: 8
- Parity: None
- Stop bits: 1
- Flow control: None
### Frame Format
```
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
```
### Special Bytes
| Byte | Name | Value | Description |
|------|------|-------|-------------|
| FEND | Frame End | 0xC0 | Frame delimiter |
| FESC | Frame Escape | 0xDB | Escape character |
| TFEND | Transposed FEND | 0xDC | FESC + TFEND represents 0xC0 in data |
| TFESC | Transposed FESC | 0xDD | FESC + TFESC represents 0xDB in data |
### Byte Stuffing
To send a data byte of 0xC0 (FEND), emit: 0xDB 0xDC (FESC TFEND).
To send a data byte of 0xDB (FESC), emit: 0xDB 0xDD (FESC TFESC).
### Type Byte
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Port (4) | Command (4) |
+---+---+---+---+---+---+---+---+
```
Port is 0 for single-port TNC (standard for MeshCore).
### Standard KISS Commands (Host to TNC)
| Command | Value | Data | Description |
|---------|-------|------|-------------|
| DataFrame | 0x00 | Raw packet | Queue packet for transmission |
| TXDELAY | 0x01 | 1 byte | Transmitter keyup delay (× 10ms) |
| Persistence | 0x02 | 1 byte | CSMA persistence (0-255) |
| SlotTime | 0x03 | 1 byte | CSMA slot interval (× 10ms) |
| TXtail | 0x04 | 1 byte | Post-TX hold time (× 10ms) |
| FullDuplex | 0x05 | 1 byte | 0=half, nonzero=full |
| SetHardware | 0x06 | Sub-cmd + data | MeshCore extensions |
| Return | 0xFF | — | Exit KISS mode (no-op) |
### TNC to Host
| Type | Value | Data | Description |
|------|-------|------|-------------|
| DataFrame | 0x00 | Raw packet | Received packet from radio |
Data frames carry raw MeshCore packets (up to 255 bytes, MAX_TRANS_UNIT).
### SetHardware Extensions (0x06)
MeshCore extends the KISS protocol via the SetHardware command. The first data
byte is a sub-command identifier.
**Request Sub-commands (Host to TNC):**
| Sub-cmd | Value | Data | Description |
|---------|-------|------|-------------|
| GetIdentity | 0x01 | — | Get node's Ed25519 public key |
| GetRandom | 0x02 | len(1) | Get random bytes (1-64) |
| VerifySignature | 0x03 | pubkey(32)+sig(64)+data | Verify Ed25519 signature |
| SignData | 0x04 | data | Ed25519 sign |
| EncryptData | 0x05 | key(32)+plaintext | AES-128 encrypt |
| DecryptData | 0x06 | key(32)+mac(2)+ciphertext | AES-128 decrypt with MAC |
| KeyExchange | 0x07 | remote_pub(32) | X25519 ECDH |
| Hash | 0x08 | data | SHA-256 hash |
| SetRadio | 0x09 | freq(4)+bw(4)+sf(1)+cr(1) | Set radio parameters |
| SetTxPower | 0x0A | power(1) | Set TX power (dBm) |
| GetRadio | 0x0B | — | Get radio parameters |
| GetTxPower | 0x0C | — | Get TX power |
| GetCurrentRssi | 0x0D | — | Get current RSSI |
| IsChannelBusy | 0x0E | — | Check if channel busy |
| GetAirtime | 0x0F | pkt_len(1) | Estimate air time |
| GetNoiseFloor | 0x10 | — | Get noise floor |
| GetVersion | 0x11 | — | Get firmware version |
| GetStats | 0x12 | — | Get RX/TX statistics |
| GetBattery | 0x13 | — | Get battery voltage |
| GetMCUTemp | 0x14 | — | Get MCU temperature |
| GetSensors | 0x15 | perms(1) | Get sensor data |
| GetDeviceName | 0x16 | — | Get device name |
| Ping | 0x17 | — | Ping |
| Reboot | 0x18 | — | Reboot device |
| SetSignalReport | 0x19 | enable(1) | Enable/disable RxMeta |
| GetSignalReport | 0x1A | — | Get signal report status |
**Response Sub-commands (TNC to Host):**
Response codes: `response = command | 0x80`
| Sub-cmd | Value | Data |
|---------|-------|------|
| Identity | 0x81 | pubkey(32) |
| Random | 0x82 | random_bytes(1-64) |
| Verify | 0x83 | result(1): 0=invalid, 1=valid |
| Signature | 0x84 | signature(64) |
| Encrypted | 0x85 | mac(2)+ciphertext |
| Decrypted | 0x86 | plaintext |
| SharedSecret | 0x87 | secret(32) |
| HashResult | 0x88 | hash(32) |
| Radio | 0x8B | freq(4)+bw(4)+sf(1)+cr(1) |
| TxPower | 0x8C | power(1) |
| CurrentRssi | 0x8D | rssi(1, signed) |
| ChannelBusy | 0x8E | busy(1): 0=clear, 1=busy |
| Airtime | 0x8F | millis(4) |
| NoiseFloor | 0x90 | dBm(2, signed) |
| Version | 0x91 | version(1)+reserved(1) |
| Stats | 0x92 | rx(4)+tx(4)+errors(4) |
| Battery | 0x93 | millivolts(2) |
| MCUTemp | 0x94 | temp(2, signed, tenths °C) |
| Sensors | 0x95 | CayenneLPP data |
| DeviceName | 0x96 | name(UTF-8) |
| Pong | 0x97 | — |
| SignalReport | 0x9A | status(1) |
| OK | 0xF0 | — |
| Error | 0xF1 | error_code(1) |
| TxDone | 0xF8 | result(1): 0=fail, 1=success |
| RxMeta | 0xF9 | snr(1)+rssi(1) |
### Error Codes
| Code | Value | Description |
|------|-------|-------------|
| InvalidLength | 0x01 | Request data too short |
| InvalidParam | 0x02 | Invalid parameter value |
| NoCallback | 0x03 | Feature not available |
| MacFailed | 0x04 | MAC verification failed |
| UnknownCmd | 0x05 | Unknown sub-command |
| EncryptFailed | 0x06 | Encryption failed |
### Unsolicited Events
**TxDone (0xF8)**: Sent after packet transmission. 0x01 = success, 0x00 = fail.
**RxMeta (0xF9)**: Sent after each received data frame. Contains:
- SNR: 1 byte, signed, × 4 for 0.25 dB precision
- RSSI: 1 byte, signed, dBm
### Data Format Notes
- Maximum payload per frame: 255 bytes (MAX_TRANS_UNIT)
- Frames larger than 255 unescaped bytes are silently dropped
- All multi-byte integers are little-endian
- Radio frequency is in Hz (e.g., 869618000)
- Battery voltage in millivolts
- MCU temperature in tenths of °C (e.g., 253 = 25.3°C)
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet format within data frames
- [Section 14: Cryptography](14-crypto.md) — Crypto operations via SetHardware
- Test vectors: `corpus/kiss/`
### Reference Implementation
- `docs/kiss_modem_protocol.md` in the MeshCore repository
+69
View File
@@ -0,0 +1,69 @@
# MeshCore Protocol Specification
## Section 20: Bridge Protocol
### Overview
The bridge protocol enables MeshCore packet forwarding over non-LoRa transports
such as RS232 serial links and ESP-NOW WiFi. It adds a simple framing layer
with a magic number and checksum around the raw MeshCore packet.
### Frame Format
```
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Magic (2B) │ Payload │ Checksum (2B)│ │
│ 0xC03E │ (variable) │ Fletcher-16 │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
```
### Fields
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Magic | 2 bytes | uint16 | `0xC03E` — identifies the frame as a MeshCore bridge packet |
| Payload | variable | raw | Raw MeshCore packet (as produced by `Packet::writeTo()`) |
| Checksum | 2 bytes | Fletcher-16 | Fletcher-16 checksum over the payload |
### RS232 Variant
The RS232 variant adds a length field between the magic and payload:
```
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Magic (2B) │ Length (2B) │ Payload │ Checksum (2B)│
│ 0xC03E │ uint16_le │ (variable) │ Fletcher-16 │
└──────────────┴──────────────┴──────────────┴──────────────┘
```
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Length | 2 bytes | uint16_le | Length of the payload in bytes |
### Fletcher-16 Checksum
The Fletcher-16 checksum is computed over the payload bytes:
```
function fletcher16(data):
sum1 = 0
sum2 = 0
for byte in data:
sum1 = (sum1 + byte) mod 255
sum2 = (sum2 + sum1) mod 255
return (sum2 << 8) | sum1
```
### ESP-NOW Variant
For ESP-NOW transport, the frame format is the same as the basic format (magic +
payload + checksum) without the length field, since ESP-NOW provides its own
length framing.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet format within payload
### Reference Implementation
- Bridge handling in MeshCore firmware (board-specific implementations)
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env python3
"""
MeshCore Spec Corpus Validator
Validates all test vector JSON files against the schema and checks internal
consistency of test vectors (hex encoding, field relationships, etc.).
Usage:
python tools/validate.py [--verbose] [path...]
If no paths given, validates all JSON files under corpus/.
"""
import argparse
import json
import re
import struct
import sys
from pathlib import Path
# Schema validation is optional (requires jsonschema)
try:
import jsonschema
HAS_JSONSCHEMA = True
except ImportError:
HAS_JSONSCHEMA = False
REPO_ROOT = Path(__file__).resolve().parent.parent
CORPUS_DIR = REPO_ROOT / "corpus"
SCHEMA_PATH = CORPUS_DIR / "schema" / "test-vector.schema.json"
# Maps payload type names to their numeric values
PAYLOAD_TYPE_MAP = {
"request": 0x00, "response": 0x01, "txt_msg": 0x02, "ack": 0x03,
"advert": 0x04, "grp_txt": 0x05, "grp_data": 0x06, "anon_req": 0x07,
"path": 0x08, "trace": 0x09, "multipart": 0x0A, "control": 0x0B,
"raw_custom": 0x0F,
}
ROUTE_TYPE_MAP = {
"transport_flood": 0x00, "flood": 0x01, "direct": 0x02,
"transport_direct": 0x03,
}
# Wire format sizes (bytes)
HEADER_SIZE = 1
TRANSPORT_CODES_SIZE = 4
PATH_LEN_SIZE = 1
def strip_hex(s):
"""Remove whitespace from hex string and return uppercase."""
return re.sub(r'\s+', '', s).upper()
def hex_to_bytes(s):
"""Convert hex string (with optional whitespace) to bytes."""
return bytes.fromhex(strip_hex(s))
def compute_header(version, payload_type, route_type):
"""Compute the header byte from structured fields."""
pt = PAYLOAD_TYPE_MAP.get(payload_type)
rt = ROUTE_TYPE_MAP.get(route_type)
if pt is None or rt is None:
return None
return (version << 6) | (pt << 2) | rt
def compute_path_len_byte(hash_size, hash_count):
"""Compute the path_len byte from hash_size and hash_count."""
return ((hash_size - 1) << 6) | (hash_count & 0x3F)
class ValidationError:
"""A single validation error with file location and optional vector context."""
def __init__(self, file_path, vector_id, message):
self.file_path = file_path
self.vector_id = vector_id
self.message = message
def __str__(self):
loc = str(self.file_path)
if self.vector_id:
loc += f" [{self.vector_id}]"
return f" ERROR: {loc}: {self.message}"
def validate_json_syntax(file_path):
"""Validate that the file is valid JSON."""
try:
with open(file_path, encoding="utf-8") as f:
return json.load(f), []
except FileNotFoundError:
return None, [ValidationError(file_path, None, f"File not found: {file_path}")]
except json.JSONDecodeError as e:
return None, [ValidationError(file_path, None, f"Invalid JSON: {e}")]
def validate_schema(data, file_path, schema):
"""Validate against JSON Schema."""
errors = []
try:
jsonschema.validate(data, schema)
except jsonschema.ValidationError as e:
errors.append(ValidationError(file_path, None, f"Schema: {e.message}"))
return errors
def validate_hex_string(hex_str, field_name, file_path, vector_id):
"""Validate a hex string is well-formed."""
errors = []
stripped = strip_hex(hex_str)
if len(stripped) % 2 != 0:
errors.append(ValidationError(file_path, vector_id,
f"Odd-length hex in {field_name}: '{stripped}'"))
if not re.match(r'^[0-9A-F]*$', stripped):
errors.append(ValidationError(file_path, vector_id,
f"Invalid or lowercase hex chars in {field_name} (must be uppercase)"))
return errors
def validate_vector_consistency(vector, file_path):
"""Validate internal consistency of a single test vector."""
errors = []
vid = vector.get("id", "?")
vtype = vector.get("type")
# Validate binary hex
if "binary" in vector:
errors.extend(validate_hex_string(vector["binary"], "binary", file_path, vid))
# For encode_decode vectors, verify header byte matches structured form
if vtype in ("encode_decode", "decode_only") and "structured" in vector and "binary" in vector:
structured = vector["structured"]
binary = hex_to_bytes(vector["binary"])
if len(binary) < 1:
errors.append(ValidationError(file_path, vid, "Binary too short for header"))
return errors
# Check header byte
hdr = structured.get("header", {})
expected_header = compute_header(
hdr.get("version", 0),
hdr.get("payload_type", ""),
hdr.get("route_type", "")
)
if expected_header is not None and binary[0] != expected_header:
errors.append(ValidationError(file_path, vid,
f"Header mismatch: binary[0]=0x{binary[0]:02X}, "
f"expected=0x{expected_header:02X}"))
# Check transport codes presence and values
rt = hdr.get("route_type", "")
has_tc = rt in ("transport_flood", "transport_direct")
offset = HEADER_SIZE
if has_tc:
if "transport_codes" not in structured:
errors.append(ValidationError(file_path, vid,
"Route type requires transport_codes but none in structured"))
elif len(binary) >= HEADER_SIZE + TRANSPORT_CODES_SIZE:
# Verify transport code LE values
tc = structured["transport_codes"]
tc0 = struct.unpack_from('<H', binary, HEADER_SIZE)[0]
tc1 = struct.unpack_from('<H', binary, HEADER_SIZE + 2)[0]
if tc0 != tc[0]:
errors.append(ValidationError(file_path, vid,
f"transport_code[0] mismatch: binary={tc0}, structured={tc[0]}"))
if tc1 != tc[1]:
errors.append(ValidationError(file_path, vid,
f"transport_code[1] mismatch: binary={tc1}, structured={tc[1]}"))
offset += TRANSPORT_CODES_SIZE
else:
if "transport_codes" in structured:
errors.append(ValidationError(file_path, vid,
"Route type forbids transport_codes but present in structured"))
# Check path_len byte
path_info = structured.get("path", {})
if offset < len(binary):
expected_pl = compute_path_len_byte(
path_info.get("hash_size", 1),
path_info.get("hash_count", 0)
)
if binary[offset] != expected_pl:
errors.append(ValidationError(file_path, vid,
f"path_len mismatch: binary[{offset}]=0x{binary[offset]:02X}, "
f"expected=0x{expected_pl:02X}"))
# Verify total binary length
path_bytes = path_info.get("hash_count", 0) * path_info.get("hash_size", 1)
min_expected = (HEADER_SIZE + (TRANSPORT_CODES_SIZE if has_tc else 0)
+ PATH_LEN_SIZE + path_bytes + 1)
if len(binary) < min_expected:
errors.append(ValidationError(file_path, vid,
f"Binary too short: {len(binary)} bytes, minimum expected {min_expected}"))
# Check hash array length matches hash_count
hashes = path_info.get("hashes", [])
if len(hashes) != path_info.get("hash_count", 0):
errors.append(ValidationError(file_path, vid,
f"hashes array length ({len(hashes)}) != hash_count ({path_info.get('hash_count', 0)})"))
# Check each hash is the right size
hs = path_info.get("hash_size", 1)
for i, h in enumerate(hashes):
h_bytes = len(strip_hex(h)) // 2
if h_bytes != hs:
errors.append(ValidationError(file_path, vid,
f"hash[{i}] is {h_bytes} bytes, expected {hs}"))
# Validate crypto_context hex fields
if "crypto_context" in vector:
ctx = vector["crypto_context"]
for field in ("shared_secret", "encryption_key", "plaintext",
"sender_private_key", "sender_public_key",
"recipient_private_key", "recipient_public_key"):
if field in ctx:
errors.extend(validate_hex_string(
ctx[field], f"crypto_context.{field}", file_path, vid))
return errors
def validate_file(file_path, schema=None, verbose=False):
"""Validate a single test vector file."""
errors = []
# JSON syntax
data, parse_errors = validate_json_syntax(file_path)
errors.extend(parse_errors)
if data is None:
return errors, 0
# Schema validation
if schema and HAS_JSONSCHEMA:
errors.extend(validate_schema(data, file_path, schema))
# Required fields
if "vectors" not in data:
errors.append(ValidationError(file_path, None, "Missing 'vectors' array"))
return errors, 0
# Check for duplicate IDs
ids = [v.get("id") for v in data["vectors"] if "id" in v]
seen = set()
for vid in ids:
if vid in seen:
errors.append(ValidationError(file_path, vid, f"Duplicate vector ID: {vid}"))
seen.add(vid)
# Validate each vector
for vector in data["vectors"]:
errors.extend(validate_vector_consistency(vector, file_path))
n = len(data["vectors"])
if verbose and not errors:
print(f" OK: {file_path} ({n} vectors)")
return errors, n
def main():
"""Entry point: parse arguments, discover test vector files, validate, and report results."""
parser = argparse.ArgumentParser(description="Validate MeshCore Spec corpus")
parser.add_argument("paths", nargs="*", help="Specific files/dirs to validate")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
# Load schema
schema = None
if HAS_JSONSCHEMA and SCHEMA_PATH.exists():
with open(SCHEMA_PATH, encoding="utf-8") as f:
schema = json.load(f)
# Find files to validate
if args.paths:
files = []
for p in args.paths:
p = Path(p)
if p.is_dir():
files.extend(p.rglob("*.json"))
elif p.is_file():
files.append(p)
else:
files = sorted(CORPUS_DIR.rglob("*.json"))
# Exclude schema files
files = [f for f in files if "schema" not in str(f)]
if not files:
print("No files to validate.")
return 0
print(f"Validating {len(files)} files...")
if not HAS_JSONSCHEMA:
print(" (jsonschema not installed - skipping schema validation)")
print(" Install with: pip install jsonschema")
all_errors = []
total_vectors = 0
for f in sorted(files):
errors, n = validate_file(f, schema, args.verbose)
all_errors.extend(errors)
total_vectors += n
# Report
print()
if all_errors:
print(f"FAILED: {len(all_errors)} error(s) in {len(files)} files "
f"({total_vectors} vectors)")
for e in all_errors:
print(e)
return 1
else:
print(f"PASSED: {len(files)} files, {total_vectors} vectors, 0 errors")
return 0
if __name__ == "__main__":
sys.exit(main())