PROTOCOL_VERSION read 2 with a note saying 3 would be wrong until ML-KEM was in
every ratchet step, "a separate piece of work". That work is done and proven
against another engine, so the number moves in the same change as the evidence
rather than ahead of it.
Until now the ratchet was inert in the field: iOS advertises 3 and gates on the
peer doing the same, so an iOS-to-Android session negotiated v2 and no message
ever carried a ratchet step, whatever either engine implemented.
🔴 A version is a claim other devices act on. Peers pin the highest proven
version and the pin is monotonic (§13.4), so a build that advertises 3 and
cannot honour it does not degrade — it strands every contact that believed it.
Three tests moved with it, and one is new. `a v2 registration publishes the
whole block` asserted 2 and now asserts the constant. `a capability signature
does not transfer between versions` edited a bundle to `proto = 3` to prove the
signature would not follow — a hardcoded neighbour that stopped testing anything
the moment the engine published 3, so it uses `PROTOCOL_VERSION + 1`. The v2
gating test needed a peer that has genuinely proven 2, which this engine no
longer produces: `copy(proto = 2)` is not enough, because the capability
signature commits to the version, so `publishedV2` re-signs for it.
The new one checks the published bundle claims 3 AND proves it. Bumping the
constant without the signature following would produce a bundle every peer
silently reads as v1, with nothing reporting an error anywhere.
Also: `the sealed blob reveals nothing` was the same coin flip as its iOS twin —
sealing ids 41 and 42 and searching the BASE64 for "41" and "42", about a 14%
chance of a meaningless failure. It fired during this run. The iOS one was fixed
this morning and this was missed because only one engine was looked at.
606 tests, 0 failures. Cross-engine: 56 passed, 0 failed.