Status: in progress
An MQTT 5.0 control packet reader and printer in Lean,
covering the seven packet types CONNECT, PUBLISH, SUBSCRIBE,
SUBACK, PINGREQ, PINGRESP, and DISCONNECT, with their
property blocks. The reader reads one packet from the front
of a byte list and returns the rest of the input, so a
caller can walk a stream of packets. The definitions are in
Mqtt/Value.lean (the packet types, the property table,
the printer, and the well-formedness rules), Mqtt/Parse.lean
(the reader), and Mqtt/Utf8.lean (a strict UTF-8 codec,
copied from Proven-Commons/jws#2, a draft pull request not
yet merged, with its namespace renamed; its proof is
included here as Proofs/Utf8.lean). The
rules are written out in SPEC.md. This slice is framing
only: no broker, no client, and no session behaviour.
The statements a reviewer approves are in Spec.lean,
with their proofs in Proofs/SpecProofs.lean. No reviewer
has approved them yet, so this project is not called
verified.
lake build builds the library, the proofs, and the
command line tool. The tool reads one request per line:
the word parse, a space, and the packet as
comma-separated decimal byte codes. It answers ok: and
the byte codes of the canonical reprint when the bytes are
one packet, none when they are rejected, and error when
the request itself is malformed.
lake build
echo "parse 192,0" | .lake/build/bin/mqtt
That request is a PINGREQ, and the answer is ok:192,0: the
packet read back and reprinted. A PINGREQ carrying a body
(parse 192,1,0) answers none. See SPEC.md for the full
protocol.
python3 tests/run_tests.py rebuilds the tool, runs the
fixed cases, the differential run against the harness
oracle, the Paho cross-check when the library is
importable, the mutation and rejection cases, and random
packets of each forbidden class drawn from the seed, and
writes the outcome to tests/TEST-RESULTS.md: the
oracle, the seed, the input count and the failures.
python3 tests/plant_bugs.py plants small bugs in the
reader and its rules, one at a time in a scratch copy of
the repo: a five byte cap, longer writings of a variable
byte integer accepted, short input padded with zeros,
partial weakenings of the filter and value rules, and
more. For each it names the approved statement the plant
makes false, checks that the test harness reports a
failure and that the proofs no longer build, and writes
the outcome to tests/PLANTED-BUGS.md.
Each claim below names the theorem behind it. The
theorems are stated in Spec.lean (the challenge file,
where they end in sorry) and proved in
Proofs/SpecProofs.lean, under the same names.
comparator.json lists all of them. A claim that names
its input, or says "one example input", is a theorem
about that input alone, which a reader can check by
evaluation; the rule it illustrates is stated for every
input by the theorem it names after "every input". The
rules those theorems state are written in
Spec/Defs.lean, one of the spec files an approval
covers.
| Claim | Theorem |
|---|---|
| Parsing the printed form of a well formed packet, with more stream bytes after it, gives the packet and those bytes back. | Spec.parsePacket_print |
| Parsing the printed form of a well formed packet on its own gives the packet back. | Spec.parse_print |
Every packet the whole input reader parse accepts is well formed, and printing it gives back exactly the bytes it read. With the row above, parse accepts a byte list exactly when it is the printing of a well formed packet, and refuses every other byte list. |
Spec.parse_sound |
Every packet the stream reader parsePacket takes from the front of a byte list is well formed, and the bytes it took are exactly its printing, followed by the bytes it returns: it never takes a byte the input does not hold. |
Spec.parsePacket_sound |
| Reading a printed variable byte integer gives the number back, for every value below 2^28. | Spec.decVBI_printVBI |
What the variable byte integer reader decVBI accepts is exactly the printer's writing of the value it returns, followed by the bytes it returns; the value is below 2^28. So each value has one writing the reader accepts. |
Spec.decVBI_canonical |
The printer writes every value below 2^28 in Spec.vbiSize bytes, the fewest that carry it: one byte below 2^7, two below 2^14, three below 2^21, four below 2^28 (Spec.vbiSize is written in Spec/Defs.lean). |
Spec.printVBI_length |
For every input, what decVBI accepts uses exactly Spec.vbiSize bytes for the value it returns. So a writing longer than the value needs, such as 128, 0 for 0 or 128, 129, 0 for 128, is refused (section 1.5.5, MQTT-1.5.5-1). |
Spec.decVBI_length |
Every packet the stream reader parsePacket takes is a header byte, the printer's writing of the body length (the shortest, by the row above), and that many body bytes, all standing in the input; the bytes it returns are exactly the ones after them. |
Spec.parsePacket_frame |
The packet 192, 128, 0 (a PINGREQ whose remaining length 0 is written in two bytes) is refused, while 192, 0 is the PINGREQ (every input: Spec.decVBI_length with Spec.parsePacket_frame). |
Spec.rejects_non_minimal_remaining_length |
| A writing longer than the value needs is refused in the other places a variable byte integer stands, one example input each: the PUBLISH 48, 5, 0, 1, 97, 128, 0 (property length 0 written 128, 0), the PUBLISH 48, 7, 0, 1, 97, 3, 129, 0, 1 (property identifier 1 written 129, 0), and the SUBSCRIBE 130, 11, 0, 1, 3, 11, 129, 128, 0, 0, 1, 97, 0 (Subscription Identifier 1 written 129, 128, 0); the same packets with the shortest writing are accepted. No statement covers every input in these places (see What is not proved). | Spec.rejects_non_minimal_property_vbis |
The printer writes each value of the standard's worked table in section 1.5.5 as the bytes the table gives: 0 as 0, 127 as 127, 128 as 128, 1, 16383 as 255, 127, 16384 as 128, 128, 1, 2097151 as 255, 255, 127, 2097152 as 128, 128, 128, 1, and 268435455 as 255, 255, 255, 127; and decVBI reads each of those writings as its value. These rows only. |
Spec.vbi_worked_table |
The variable byte integer reader decVBI refuses every input whose first four bytes all carry a continuation mark, whatever follows: the value would need a fifth byte, and four is the limit. |
Spec.decVBI_four_byte_cap |
A packet whose remaining length has a continuation mark on each of its first four bytes is refused, by the stream reader parsePacket and by the whole input reader parse, whatever the header byte and whatever follows. |
Spec.parsePacket_four_byte_cap |
The whole input reader parse accepts a byte list, and returns a packet, exactly when the stream reader parsePacket takes that packet from it and leaves no byte after it. So each rule below, stated for parsePacket, holds for parse too. |
Spec.parse_whole |
For every input, each packet parsePacket takes starts with one of the sixteen header bytes of Spec.headerBytes: 0x10, 0x30 to 0x35, 0x3A to 0x3D, 0x82, 0x90, 0xC0, 0xD0 and 0xE0. So a SUBSCRIBE whose flags are not 0010, another type whose flags are not 0000, a PUBLISH with QoS 3 or with DUP set at QoS 0, and every packet type outside the seven of this slice are refused. |
Spec.header_rule |
For every input, each CONNECT parsePacket takes is the byte 0x10, the body length, and a body that starts with the protocol name MQTT (0, 4, 77, 81, 84, 84), the level 5, and a connect flags byte that keeps Spec.ConnectFlagsRule: the reserved bit 0 clear, a will QoS other than 3, and, when the will flag is clear, the will QoS and will retain bits clear. A will topic is not empty and holds no + or #. |
Spec.connect_rule |
For every input, each PUBLISH parsePacket takes has a topic name with no + or #, which is empty only when its properties carry a Topic Alias; its packet identifier stands exactly when the QoS is not 0, and is never 0. |
Spec.publish_rule |
For every input, each SUBSCRIBE parsePacket takes has a packet identifier other than 0 and at least one topic filter, and each filter keeps Spec.FilterRule. A filter is not empty; split at each /, a level that holds + is + alone, and a level that holds # is # alone and the last level. A filter that starts with $share/ has a ShareName of at least one character with no /, + or #, then a / and a filter that keeps those rules, and its No Local option is clear. |
Spec.subscribe_rule |
For every input, every property block of every packet parsePacket takes keeps Spec.BlockRule. Each property is allowed on its block (Table 2-4) and has its identifier's value type. Each value keeps the value rules: no Subscription Identifier, Topic Alias, Receive Maximum or Maximum Packet Size of 0, a Request Problem Information and Request Response Information of 0 or 1, and a Response Topic that is not empty and holds no + or #. No single use property repeats in a block, and Authentication Data stands only with an Authentication Method. |
Spec.property_rule |
For every input, each PINGREQ parsePacket takes is the two bytes 192, 0, and each PINGRESP the two bytes 208, 0. |
Spec.empty_body_rule |
The SUBSCRIBE 128, 7, 0, 1, 0, 0, 1, 97, 0, whose flags are 0000 rather than the 0010 the standard fixes, is refused (one example input; every input: Spec.header_rule). |
Spec.rejects_subscribe_flags |
A PUBLISH with the reserved QoS value 3 is refused (one example input, header 0x36; every input: Spec.header_rule). |
Spec.rejects_publish_qos3 |
A PUBLISH with the DUP flag set at QoS 0 is refused: here the packet 56, 4, 0, 1, 97, 0 (header 0x38). The same packet with header 0x30 is accepted (every input: Spec.header_rule). |
Spec.rejects_dup_at_qos0 |
A PUBLISH with an empty topic name and a Topic Alias (here 48, 6, 0, 0, 3, 35, 0, 1, alias 1) is accepted, and the same packet with no Topic Alias (48, 3, 0, 0, 0) is refused (every input: Spec.publish_rule). |
Spec.empty_topic_needs_alias |
A PUBLISH whose topic name is a/# is refused (one example input; every input: Spec.publish_rule). |
Spec.rejects_wildcard_topic |
A CONNECT whose protocol name is MQTU, not MQTT, is refused (one example input; every input: Spec.connect_rule). |
Spec.rejects_wrong_protocol_name |
A CONNECT whose connect flags byte is 0x03, with the reserved bit 0 set, is refused (one example input; every input: Spec.connect_rule). |
Spec.rejects_reserved_connect_flag |
A CONNECT whose will flag is clear but whose will QoS or will retain bit is set is refused: here the connect flags 0x0A and 0x22 on an otherwise valid CONNECT (every input: Spec.connect_rule). |
Spec.rejects_will_bits_without_will |
A CONNECT carrying a Subscription Identifier, which its property block does not allow, is refused (one example input; every input: Spec.property_rule). |
Spec.rejects_property_wrong_type |
A PUBLISH carrying the single use Topic Alias twice is refused (one example input; every input: Spec.property_rule). |
Spec.rejects_repeated_property |
SUBSCRIBE topic filters that break the standard's filter rules are refused, one example input each: a/#/b (# not last), a+ (+ not alone in its level), the empty filter, $share/g/t with No Local set, and $share/g (no filter after the ShareName). The filters a/+/# and $share/g/t with No Local clear are accepted (every input: Spec.subscribe_rule). |
Spec.rejects_bad_topic_filters |
A SUBSCRIBE with packet identifier 0 is refused (one example input; every input: Spec.subscribe_rule). |
Spec.rejects_subscribe_id_zero |
Property values the standard calls a Protocol Error are refused, one example input each: a Subscription Identifier of 0 on a SUBSCRIBE, a Topic Alias of 0 on a PUBLISH, a Receive Maximum of 0, a Maximum Packet Size of 0, a Request Problem Information of 2, a Request Response Information of 2, and Authentication Data with no Authentication Method on a CONNECT, and a Response Topic # and an empty Response Topic on a PUBLISH (every input: Spec.property_rule). |
Spec.rejects_property_value_rules |
| The DISCONNECT 224, 1, 0 (a reason code with no property block) reads as that form and prints back as the same three bytes, and the DISCONNECT 224, 2, 0, 0 (a reason code and an empty property block) reads as that form and prints back as the same four bytes. | Spec.disconnect_short_form |
The PINGREQ 192, 1, 0, which carries a one byte body, is refused (one example input; every input: Spec.empty_body_rule). |
Spec.rejects_pingreq_body |
The packet 32, 0, of type 2, outside the seven in this slice, is refused (one example input; every input: Spec.header_rule). |
Spec.rejects_unknown_type |
A packet is well formed when its numbers fit the widths
that carry them, its strings obey the string rules and fit
the two byte length prefix, its properties match the
property table in type and place, keep the value rules
the standard sets (no Subscription Identifier, Topic
Alias, Receive Maximum or Maximum Packet Size of 0, and
so on, as SPEC.md lists), and repeat no single use
property, its packet identifiers stand where the
type requires them and are never 0 there, a PUBLISH at
QoS 0 has DUP clear, a PUBLISH topic name holds no
wildcard and is empty only beside a Topic Alias, its
SUBSCRIBE topic filters keep the filter rules, and its
whole body is shorter than 2^28, the largest remaining
length a variable byte integer can carry. The full list is in
SPEC.md. Well formedness is Mqtt.PacketWF in
Mqtt/Value.lean, which is code under proof: an approval
pins the statements that use it, not its text. So each
rule a claim above depends on is also written in
Spec/Defs.lean, part of the specification, and proved
for every input from there (Spec.header_rule through
Spec.empty_body_rule). The well formedness rules with
no such statement are listed under What is not proved.
The round trip theorems rest on the proofs in
Proofs/RoundTrip.lean, where they are
Mqtt.Proofs.parsePacket_print, Mqtt.Proofs.parse_print,
and Mqtt.Proofs.decVBI_printVBI. The soundness theorems
rest on Proofs/Sound.lean, where they are
Mqtt.Proofs.parse_inv and Mqtt.Proofs.parsePacket_inv,
built from one inverse lemma per field reader, and on the
inverse of the strict UTF-8 decoder,
Mqtt.Utf8.Proofs.decode_inv in Proofs/Utf8Inv.lean.
The rule theorems, and the byte count of a variable byte
integer, rest on Proofs/Rules.lean, which derives each
rule in Spec/Defs.lean from what the soundness theorems
say the reader accepts.
All of them check against the standard axioms only
(propext, Classical.choice, Quot.sound), which a
reader can confirm with #print axioms, or by running
lake env lean Mqtt/Verify.lean. The theorems about
single inputs are checked in the kernel with
decide +kernel.
Under the round trips, the field and body steps are proved
one by one in the same file: big endian numbers, byte cuts,
string and binary fields, typed property values, single
properties, property lists and whole property blocks, the
subscription options byte, topic filter lists, and the body
of each of the seven packet types. The UTF-8 round trip,
Mqtt.Utf8.Proofs.decode_encode, is proved in
Proofs/Utf8.lean, and worked examples from the standard
are proved in Proofs/Examples.lean.
The command line tool in Main.lean calls Mqtt.parse
and Mqtt.print directly, the same functions the theorems
are about.
- That well formedness and the printer match the MQTT 5.0
standard. The proofs show that the reader accepts exactly
the printings of well formed packets (
Spec.parse_printwithSpec.parse_sound). That this set is the set of byte strings the standard allows for these seven packet types is a reading of the standard:SPEC.mdstates it, a reviewer checks it against the standard, and the oracle differential and the mutation tests intests/probe it, with the outcome in the results file. It is not proved. - Where each field sits in the bytes. The rules in
Spec/Defs.leanare stated on the header byte, on the start of a CONNECT body, and on the fields of the packet the reader returns. The round trip and soundness theorems tie those fields to the bytes through the printer,Mqtt.print, whose text is code under proof; beyond the example inputs, no statement fixes the layout byte by byte. - The well formedness rules that
Spec/Defs.leandoes not restate. The reader keeps them on every input (Spec.parse_sound), but they are written only inMqtt/Value.leanandMqtt/Parse.lean, code under proof whose text an approval does not pin. They are: the string rules (strict UTF-8, no U+0000, at most 65535 bytes) and the binary length limit; the subscription options byte (bits 6 and 7 clear, QoS and Retain Handling not 3); a SUBACK with a packet identifier other than 0 and at least one reason code; and the three DISCONNECT body forms. The tests intests/check them, and the example theorems inProofs/Examples.leancheck some on single inputs. - The shortest writing of a variable byte integer inside a
property block, for every input.
Spec.decVBI_lengthstates it for the variable byte integer reader, andSpec.parsePacket_framewith it for the remaining length of every packet. For the property length, property identifiers and variable byte integer property values, the packet reader calls the same reader, but no statement says so for every input:Spec.rejects_non_minimal_property_vbiscovers one example input in each place, and the tests intests/run_tests.pycheck the cases named "vbi non-minimal ... as property length", "... as property identifier" and "... as subscription identifier". - The bytes of a variable byte integer for values outside the
standard's worked table. The statements pin how many bytes
the printer writes (
Spec.printVBI_length) and that the reader reads them back (Spec.decVBI_printVBI,Spec.decVBI_canonical), andSpec.vbi_worked_tablepins the bytes of the table's eight rows; that every other value is written seven bits per byte, low groups first, rests on the text ofMqtt.printVBIand on the tests: the tool reads and prints back Paho's writing of 120 random values below 2^28 as Subscription Identifiers, and the writings of the oracle encoder, written from the standard, in the generated packets. - Any reading of what a property means. A property is an identifier and a typed value; that a Receive Maximum of 10 asks the peer to hold back after ten packets is broker and session behaviour, and none of it is in this slice.
- Anything about packets outside the seven types, or
property identifiers outside the table in
SPEC.md, beyond their refusal (Spec.header_ruleandSpec.property_rule). - Broker or session behaviour of any kind: connections, subscriptions as state, retained messages, keep alive timing, and what any peer does with a packet.
- Worked examples on the printing side, beyond the two
DISCONNECT packets of
Spec.disconnect_short_form. The examples inProofs/Examples.leanare stated on the reading side. The round trip and soundness theorems cover the printer in general. - Reading requests and printing answers in
Main.lean: splitting lines, and the decimal number reader. The tests cover them, malformed requests included. - Speed. The code works on lists of bytes, which suits proofs, not large packets.
- The Lean kernel (Lean 4.35.0, pinned in
lean-toolchain), which checks every proof. - The Lean compiler, with the C compiler and linker shipped with it, which turns the proved definitions into the program that runs.
- The Lean runtime, which runs that program.
- The hardware the program runs on.
- Every dependency in
lake-manifest.json: the manifest lists no packages, so there are no library dependencies to trust beyond the Lean toolchain itself. - Lean's core library, including the compiled code it puts in place of some definitions the proofs reason about.
- The operating system the program runs on.
- That
SPEC.mdstates the reading of the MQTT 5.0 standard you intend. The proofs are about the reader and printer in this repo following each other exactly; the fidelity of the spec to the standard is a reading, pinned inSPEC.mdwhere the standard leaves a choice open. - The strict oracle in
tests/run_tests.py, written from the standard andSPEC.mdbeside the reader, and Paho's Python library in the parts of the differential it covers. The oracle shares the reader's reading of the standard, though some of its checks are written another way (it checks the shortest variable byte integer by writing the value again, and topic filters by splitting at/). Both are evidence, not proof. The results file names the versions used, and states plainly which oracles were and were not available. In the Paho cross-check, Paho judges answers of the Lean tool: its encoder's writings of variable byte integers, and property blocks of all six kinds, accepted and refused, judged byProperties.unpack. It does not judge whole packets. Paho departs from the standard in places, and the run checks those apart as named differences rather than take Paho's answer there: its decoder accepts variable byte integers longer than the value needs (section 1.5.5 forbids them), it refuses a Payload Format Indicator above 1 and a Maximum Packet Size above 268435455 (the standard does not), and it accepts a Response Topic with a wildcard or empty, Authentication Data without a Method, and a repeated Subscription Identifier on SUBSCRIBE (the standard calls each a Protocol Error). - The line handling in
Main.lean(splitting a request line, reading decimal codes). The tests drive it, and no proof covers it. - There are no exceptions to item 4 of the bar: the project uses none of the features it forbids.
lake comparator --paranoid: builds the challenge fileSpec.leanand the proofs in a sandbox and checks that each theorem named incomparator.jsonproves exactly the statement in the challenge file, uses no axiom but the three standard ones, and is accepted by Lean's kernel and every other kernel shipped with Lean.python3 tests/run_tests.py: the fixed cases, the differential against the strict oracle, the Paho cross-check when the library is importable, and the mutation and rejection cases, and the forbidden classes drawn from the seed, with counts and any failures written totests/TEST-RESULTS.md. The Paho cross-check runs when the interpreter can importpaho-mqtt(for example from a virtual environment), and the results file says when it could not. Its inputs are counted apart, so the recorded input count is the same either way.lake build: the library, the proofs, and the tool build clean, andlake env lean Mqtt/Verify.leanprints the axiom list for the round trip and soundness theorems.- Read
SPEC.mdagainst the MQTT 5.0 standard and check the pinned readings are the ones you want.