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:
swaitsandClaude Opus 4.7 committed 2026-05-12 09:22:56 -06:00
1 parent 277d4463c1
commit ce358996ad
1 file changed
+65 -1
+65 -1
View File
@@ -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