From 1c2007dcb5d65d065734eaa9c2a6494402f68930 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 13 Apr 2026 06:34:04 -0600 Subject: [PATCH] spec(multipart): constrain MULTIPART ACK to direct paths Add a "Multipart ACK Usage Constraints" subsection making explicit what upstream enforces in code but the spec previously left implicit: - MULTIPART-wrapped ACKs MUST only be emitted on direct-routed return paths. When no direct path is known, the sender MUST fall back to a plain PAYLOAD_TYPE_ACK via flood. Matches sendAckTo() and routeDirectRecvAcks() in upstream. - All MULTIPART and plain copies of the same ACK carry an identical 4-byte ack_crc; receivers MUST dedup by CRC at the application layer (Section 16 packet-hash dedup does not catch them because the wrapper byte makes each copy hash-distinct). - Typical inter-copy delay ~300 ms plus the direct retransmit delay; exact count via getExtraAckTransmitCount() (reference default 0). Validated against upstream meshcore-dev/MeshCore @ 1a7b361 (Mesh::routeDirectRecvAcks, BaseChatMesh::sendAckTo). Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/11-payload-multipart.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/11-payload-multipart.md b/spec/11-payload-multipart.md index 2c3f20d..374c798 100644 --- a/spec/11-payload-multipart.md +++ b/spec/11-payload-multipart.md @@ -59,6 +59,32 @@ 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. +### Multipart ACK Usage Constraints + +- A sender MUST only emit MULTIPART-wrapped ACKs on direct-routed return + paths (i.e., when a direct path to the original sender is known). When the + return path is unknown, the sender MUST fall back to emitting a plain + PAYLOAD_TYPE_ACK via flood routing. This reflects the firmware behavior in + `Mesh::routeDirectRecvAcks()` (which emits MULTIPART copies only when a + direct path is available) and `BaseChatMesh::sendAckTo()` (which falls back + to `sendFloodScoped()` when `out_path_len == OUT_PATH_UNKNOWN`). +- All MULTIPART-ACK copies and the final plain ACK for the same acknowledged + message carry an identical 4-byte `ack_crc`. Receivers MUST treat them as + the same logical ACK: the first one received satisfies the acknowledgment, + and subsequent duplicates MUST be deduplicated per + [Section 16](16-packet-hash.md) (each copy has a distinct packet hash + because the wrapper byte differs, so ordinary dedup does not suppress them + on the wire — application-level dedup by `ack_crc` is required). +- The typical inter-copy delay in the reference implementation is + approximately 300 ms plus the direct retransmit delay + (`Mesh::routeDirectRecvAcks()`); exact timing is implementation-defined. +- The number of extra copies is configured via `getExtraAckTransmitCount()` + and is implementation-defined. The firmware default is 0 (plain ACK only). + +A receiver MUST NOT infer delivery reliability from the `remaining` counter +alone: the counter is a hint about how many additional copies the sender +intends to emit, not a guarantee that they will arrive. + ### Encoding (Multipart ACK) 1. Set first byte to `(remaining << 4) | PAYLOAD_TYPE_ACK`. @@ -85,4 +111,8 @@ The `remaining` field indicates how many more ACK packets follow this one. ### Reference Implementation - `Mesh::createMultiAck()` in `src/Mesh.cpp` — Encoding +- `Mesh::routeDirectRecvAcks()` in `src/Mesh.cpp` — Direct-only gating and + inter-copy spacing +- `BaseChatMesh::sendAckTo()` in `src/helpers/BaseChatMesh.cpp` — Flood + fallback when no direct return path is known - `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_MULTIPART` — Decoding