From 5dfebafa3d4a074e44ac3bbf7b8de8f63eebbe74 Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 6 Oct 2026 12:43:19 +0200 Subject: [PATCH] test(ingestor): detect a dropped minimised client RX packet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relates to #305, the follow-up review of #298. 1. TestHandleClientPacketMinimisedAndWholeAgree asserted only a row count, with both packets at the same rx_at, so the UNIQUE(rx_pubkey, heard_key, rx_at) index collapsed them and the whole packet alone satisfied the assertion. Every "payload required" mutant stayed green. The minimised packet is now sent first, at its own rx_at, and its row is asserted on its own before the whole twin is sent; agreement and idempotency are then separate steps. 2. The three claims docs/client-rx-coverage.md makes get a test each: a minimised 0-hop advert is dropped (both the DIRECT sendZeroHop form and a flooded advert with no hops yet, plus a pubkey-only truncation), a minimised relayed advert survives via path[last], and a partially kept payload is tolerated. Each was re-checked against the firmware source first. The third needed correcting: a group payload is channel hash + cipher MAC + ciphertext, so only a remnant shorter than that 3-byte envelope is reported as undecodable — keep three bytes or more and the channel hash and MAC are published, which the doc now says. 3. The PAYLOAD_TYPE_* defines are on firmware/src/Packet.h:19-32, not 20-33. TestFirmwareLineCitationsAreAccurate keeps that honest: the citations in the test file must match a table of ranges, which runs everywhere, and the cited lines are matched against the real firmware when the (gitignored) firmware/ checkout is present. Tests only; no production file changes. --- cmd/ingestor/client_rx_minimised_test.go | 364 ++++++++++++++++++++++- docs/client-rx-coverage.md | 15 +- 2 files changed, 359 insertions(+), 20 deletions(-) diff --git a/cmd/ingestor/client_rx_minimised_test.go b/cmd/ingestor/client_rx_minimised_test.go index 8c6ca6794..dcd120f82 100644 --- a/cmd/ingestor/client_rx_minimised_test.go +++ b/cmd/ingestor/client_rx_minimised_test.go @@ -2,6 +2,11 @@ package main import ( "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" "testing" "time" ) @@ -36,7 +41,7 @@ const ( fwRouteTransportDirect = 0x03 ) -// Payload types — firmware/src/Packet.h:20-33. +// Payload types — firmware/src/Packet.h:19-32. const ( fwPayloadAdvert = 0x04 fwPayloadGrpTxt = 0x05 @@ -46,8 +51,9 @@ const ( // fwPayloadTypeShift mirrors firmware PH_TYPE_SHIFT (src/Packet.h:9). const fwPayloadTypeShift = 2 -// fwHeader builds the 1-byte header per firmware/src/Packet.h:8-12 -// (getRouteType/getPayloadType/getPayloadVer). Payload version is v1 (0). +// fwHeader builds the 1-byte header per the mask/shift defines on +// firmware/src/Packet.h:8-12 (PH_ROUTE_MASK, PH_TYPE_SHIFT, PH_VER_SHIFT, read +// back by Packet::getRouteType/getPayloadType/getPayloadVer). Version is v1 (0). func fwHeader(routeType, payloadType int) string { return fmt.Sprintf("%02x", (payloadType< len(lines) { + t.Errorf("%s is out of range: %s has %d lines", cite, r.file, len(lines)) + continue + } + for _, check := range []struct { + what string + line string + pat string + }{ + {"first", lines[r.from-1], r.firstRe}, + {"last", lines[r.to-1], r.lastRe}, + } { + if !regexp.MustCompile(check.pat).MatchString(check.line) { + t.Errorf("%s: %s line does not match %s — got %q", cite, check.what, check.pat, check.line) + } + } + } +} diff --git a/docs/client-rx-coverage.md b/docs/client-rx-coverage.md index a7888eb37..f0e6b1a5d 100644 --- a/docs/client-rx-coverage.md +++ b/docs/client-rx-coverage.md @@ -147,17 +147,24 @@ and `src/Packet.h`): - `path` — exactly `hop_count * hash_size` bytes. `path_length` must describe the bytes that are actually present; a byte count larger than the remaining buffer is rejected (dropped, not partially read). -- `payload` — omit entirely for non-adverts. A partially kept payload is tolerated too (it is simply - reported as undecodable), but there is no reason to send one. +- `payload` — omit entirely for non-adverts. A partially kept payload is tolerated too: the coverage + row is the same one the fully minimised packet writes, keyed on `path[last]`. There is no reason to + send one, and a remnant can still leak. A group payload is channel hash (1 byte) + cipher MAC + (2 bytes) + ciphertext ([firmware `docs/payloads.md`](https://github.com/meshcore-dev/MeshCore/blob/main/docs/payloads.md), + "Group text message"), so only a remnant *shorter* than that 3-byte envelope is reported as + undecodable — keep 3 bytes or more and the channel hash and the MAC are published. **Adverts are the exception: send them whole.** For a 0-hop advert the heard key *is* the advertiser's pubkey, and that lives in the advert payload — a minimised 0-hop advert carries nothing -attributable and is dropped. A *relayed* advert follows the normal path rule, so it survives +attributable and is dropped. Truncating the payload to just the 32-byte pubkey does not help either: +an advert payload is pubkey (32) + timestamp (4) + signature (64), and all 100 bytes must be present +before the pubkey is read back. A *relayed* advert follows the normal path rule, so it survives minimisation, but there is no benefit in special-casing it client-side. What minimisation does **not** change: `DIRECT`/`TRANSPORT_DIRECT` paths and `TRACE` packets stay unattributable, and 1-byte path hashes stay excluded. Dropping the payload never widens what is -recorded. Pinned by `TestHandleClientPacketMinimised*` in `cmd/ingestor/client_rx_minimised_test.go`. +recorded. Pinned by `TestHandleClientPacketMinimised*` and +`TestHandleClientPacketPartialPayloadTolerated` in `cmd/ingestor/client_rx_minimised_test.go`. ## Capture HARD RULE — only what was heard directly