spec(encrypted): clarify attempt semantics and reliable DM delivery
Two additions to the TXT_MSG section: 1. Attempt Counter Semantics. Spells out that bits 0-1 of txt_type_attempt carry attempt & 3, the input the ACK CRC depends on. For attempt > 3, upstream appends a [NUL][attempt_full] tail after the text (BaseChatMesh.cpp:410-412), recovering the full attempt value at the cost of 2 bytes of text capacity. The tail is NOT part of the ACK CRC input, so attempt=4 and attempt=0 collide on CRC and senders relying on >3 must verify the trailing byte. 2. Reliable DM Delivery. Documents the sender-side ACK tracking contract: maintain a pending-ACK table keyed by expected CRC, retry with attempt incremented, consume MULTIPART-wrapped or plain ACKs identically, and the receive-side option to cache replies per peer for duplicate-DM loss recovery. Validated against upstream meshcore-dev/MeshCore @ 1a7b361 (BaseChatMesh::composeMsgPacket, onPeerDataRecv TXT_MSG handling). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 file changed
+65
-1
@@ -75,9 +75,73 @@ 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) |
|
||||
| txt_type_attempt | 4 | 1 byte | uint8 | Bits 2-7: message type (see [Section 4](04-payload-ack.md)); bits 0-1: attempt counter (0-3). See "Attempt Counter Semantics" below. |
|
||||
| text | 5 | remaining | UTF-8 | Message text |
|
||||
|
||||
#### Attempt Counter Semantics
|
||||
|
||||
The `attempt` sub-field (bits 0-1 of `txt_type_attempt`) carries the low two
|
||||
bits of a transmission attempt counter used by the sender's retry logic. For
|
||||
a given logical TXT_MSG:
|
||||
|
||||
- The `timestamp` field MUST remain stable across all retries of the same
|
||||
message.
|
||||
- The `attempt` counter MUST be 0 on the first transmission and MUST be
|
||||
incremented by 1 on each retry.
|
||||
- Bits 0–1 of `txt_type_attempt` MUST always carry `attempt & 3`. These bits
|
||||
are part of the ACK CRC input (see
|
||||
[Section 4](04-payload-ack.md#ack-crc-computation)), so each retry of a
|
||||
message produces a distinct expected ACK CRC.
|
||||
- For attempts in `0..3`, no further encoding is needed; the plaintext ends
|
||||
with the text.
|
||||
- For attempts `> 3`, the sender MUST append a 2-byte tail
|
||||
`[NUL_terminator(1)][attempt_full(1)]` to the plaintext after the text,
|
||||
where `attempt_full` is the full 1-byte counter value (not just the low
|
||||
2 bits). The receiver locates the text by scanning for the NUL terminator
|
||||
and recovers the full attempt value from the trailing byte. Because this
|
||||
tail costs 2 bytes of plaintext capacity, the maximum text length is
|
||||
reduced by 2 bytes when `attempt > 3`.
|
||||
- The ACK CRC is computed over `timestamp(4) || txt_type_attempt(1) || text`
|
||||
only. The `[NUL][attempt_full]` tail (when present) and the implicit C
|
||||
string NUL terminator (when not) are excluded from the hash input. This
|
||||
means a retry with attempt 4 and a retry with attempt 0 produce ACK CRCs
|
||||
that match (since `4 & 3 == 0`); senders relying on attempts beyond 3
|
||||
SHOULD also verify the trailing attempt byte if they need to distinguish
|
||||
rolled-over attempts.
|
||||
|
||||
See `BaseChatMesh::composeMsgPacket()` in
|
||||
`src/helpers/BaseChatMesh.cpp` for the reference encoding.
|
||||
|
||||
### Reliable DM Delivery
|
||||
|
||||
The following sender-side behavior is informational and documents how
|
||||
interoperating implementations deliver TXT_MSG reliably. Exact timeouts,
|
||||
backoff schedules, and queue depths are implementation-defined; see
|
||||
[Section 17 — Sender Behavior](17-routing.md#sender-behavior-informational)
|
||||
for routing-layer details.
|
||||
|
||||
- **Expected-ACK tracking.** On transmit, the sender SHOULD compute the
|
||||
expected `ack_crc` (see [Section 4](04-payload-ack.md#ack-crc-computation))
|
||||
and record it in a pending-ACK table keyed by CRC. The table SHOULD bound
|
||||
entries by capacity and age out entries after the retry schedule has
|
||||
completed (plus a margin for in-flight ACKs).
|
||||
- **Retry.** If no matching ACK is received within an implementation-defined
|
||||
timeout, the sender SHOULD retransmit with `attempt` incremented (per
|
||||
"Attempt Counter Semantics" above) and re-register the new expected
|
||||
`ack_crc`. Up to 4 attempts are representable in the 2-bit `attempt`
|
||||
field.
|
||||
- **ACK consumption.** On receipt of a PAYLOAD_TYPE_ACK or a MULTIPART-wrapped
|
||||
ACK whose `ack_crc` matches a pending entry, the sender MUST treat the
|
||||
message as delivered, remove the pending-ACK entry, and cancel any queued
|
||||
retries for that CRC.
|
||||
- **Duplicate DM handling at the receiver.** A receiver MAY cache its most
|
||||
recent reply per peer keyed by the inbound DM's plaintext and, on a
|
||||
duplicate inbound DM within a short window, re-emit the cached reply as a
|
||||
loss-recovery optimization. This is not normative; the normative behavior
|
||||
is "process once and rely on [Section 16](16-packet-hash.md) dedup."
|
||||
Implementations that re-emit cached replies SHOULD suppress re-emission
|
||||
while a retry for that reply is still pending, to avoid piling on.
|
||||
|
||||
### Plaintext Format (REQUEST / RESPONSE)
|
||||
|
||||
The plaintext format for REQUEST and RESPONSE is application-defined. Common
|
||||
|
||||
Reference in new issue
Block a user