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:
79 files changed
+8923
No files matched your search
+22
@@ -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/
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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')."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user