Beacon Status — Group 22, Code 1 (proposed)

An authenticated, immediate query that tells a client what its beacon RAIDA knows about its mailbox and what the server will do with a long-poll: how many Tells are retained, when the newest was published, the server’s clock, the maximum hold it will honour, and which of the beacon commands this build supports. It never returns Tell records and never parks.

Status of this proposal

Not implemented

Group 22 is not in raidax or rest_core. This page is the design to review. It is the smallest of the three proposed commands, carries the capability bits the other two depend on, and can ship before Wait (group 22, command 2) and Ack (group 22, command 3), because its most useful field is simply “which commands do you support”.

Why it is needed

Today a client can only infer the state of its beacon from side effects, and every inference is wrong in some case:

  1. Health is judged by UDP echo. rest_core shows “RAIDA 25/25” from echo replies, which says nothing about whether the authenticated TCP notification path works or whether a poll is parked. A beacon that answers echo but refuses Ping looks healthy.
  2. “No mail” and “not reachable” look identical. A refused connection, a server-side close and a genuine empty poll all reach the client as an empty result. A client cannot show the user “your beacon has been unreachable for an hour”, because it does not know.
  3. Clock skew is invisible. A client has no way to read its beacon’s clock, so it cannot tell whether its own clock is off. This call measures recipient-versus-server skew only; sender-versus-server skew, which is what breaks the Ping/Peek cursor, is removed by Wait’s server-ordered cursor rather than measured here.
  4. Hold time and caps are compiled-in constants. The 15-minute hold, the 1 MiB response cap and the 255-record limit are not discoverable. A client that guesses wrong times out or strands mail.
  5. Capability discovery is by trial. The only way to learn whether a server supports a new command is to send it and see whether the dispatcher rejects it. With a new command group proposed, one call that answers for all of its commands avoids probing each one per beacon per session.

Beacon Status answers all five in one 50-byte request and a fixed 48-byte reply, cheap enough to send at start-up and on every health refresh.

Quick reference

Command Group22 (Beacon), proposed
Command Code1 (0x01)
Server functioncmd_beacon_status in a new cmd_beacon.c (to be written)
Request body50 bytes total: encrypted challenge(16) + qmail identity/auth(32), then plaintext terminator(2). Same as a minimum Ping body.
Success responseStatus 250 with a fixed 48-byte encrypted payload.
BlockingNever. Replies immediately.
Side effectsNone. Does not read Tell records, does not touch cursors or acknowledgements.
EncryptionRequired — AES-128-CTR, type 1, keyed by the caller’s selected coin AN for this RAIDA

Wire packet

QMail Beacon Status request packet, bytes on TCP

+-------------------------------+-----------------------------------------+----------------------+
| RAIDA request header           | encrypted request body                  | plaintext terminator |
| 32 bytes                       | 48 bytes                                | 2 bytes              |
+-------------------------------+-----------------------------------------+----------------------+
| command group=22, command=1    | challenge/CRC(16)                       | 3E 3E                |
| encryption type=1              | QMail identity/auth block(32)           |                      |
| body_size=50                   |                                         |                      |
| nonce(8)                       |                                         |                      |
+-------------------------------+-----------------------------------------+----------------------+

The request header is the standard QMail header (see the Ping page) with command group 0x16, command code 0x01 and body size 00 32 (50).

Request body (50 bytes)

The body is exactly the 48-byte qmail_preamble_t that every QMail command carries, plus the terminator. Authentication is the same AN check as Ping: only the mailbox owner can query its beacon status. Byte 31 (Device ID) is echoed back in the reply so a client can confirm which device identity the server saw.

Decrypted request body layout

+-------------------+--------------------------+----------------------+
| challenge/CRC     | QMail identity/auth      | terminator           |
| 16 bytes          | 32 bytes                 | 2 bytes              |
| encrypted         | encrypted                | plaintext            |
+-------------------+--------------------------+----------------------+
| body[0..15]       | body[16..47]             | body[48..49]         |
+-------------------+--------------------------+----------------------+

Response packet

QMail Beacon Status response, bytes on TCP

+-------------------------------+-----------------------------------------+----------------------+
| RAIDA response header          | encrypted payload                       | plaintext terminator |
| 32 bytes                       | 48 bytes                                | 2 bytes              |
+-------------------------------+-----------------------------------------+----------------------+
| status=250, group=6            | status fields                           | 3E 3E                |
| body_size=50                   |                                         |                      |
+-------------------------------+-----------------------------------------+----------------------+
Payload offsetSizeFieldDescription
01versionPayload layout version, 0x01. A client that sees a higher value reads only the fields it knows.
11device_id_seenPreamble byte 31 as the server read it (zero mapped to 1).
2–32commandsBig-endian bit set of beacon commands this build serves: bit 0 = 6/72 Ping, bit 1 = 6/73 Peek, bit 2 = 22/2 Wait, bit 3 = 22/3 Ack, bit 4 = 22/1 Beacon Status (always set), bit 5 = retained Tells (fetch does not delete), bit 6 = per-watcher expiry deadlines (see sweep_seconds). Bits 7–15 reserved, zero.
4–74server_timeBig-endian Unix seconds on the beacon. The client compares with its own clock to measure its own skew from the server. This says nothing about senders’ clocks.
8–114retained_countNumber of Tells currently retained for this mailbox on this server.
12–154oldest_publishServer publish time of the oldest retained Tell, or 0 when none.
16–194newest_publishServer publish time of the newest retained Tell, or 0 when none. A client whose cursor is older than this has mail to fetch.
20–278newest_cursorThe Wait cursor of the newest retained Tell, or zeros. Lets a client that only wants “new from now” start Wait without replaying the backlog. Zeros when Wait is not supported.
28–314unacked_countRetained Tells not acknowledged by the requesting device through Ack (group 22, command 3). Equal to retained_count when Ack is not supported.
32–332max_hold_secondsLongest hold the server grants a long-poll (900 in the current design). A Ping client sets its socket timeout above this plus the sweep interval; a Wait client sends a value below its own timeout.
34–352sweep_secondsHow often expired long-polls are reaped (60 today), or 0 when the server expires each poll at its own deadline. Worst-case reply latency for an idle poll is max_hold_seconds + sweep_seconds.
36–394max_response_bytesTell-list byte cap (1,048,576 today).
401max_recordsTell-list record cap per reply once the server enforces one. Today there is no cap and the one-byte count wraps, so a current server would report 0 here.
411parked_pollsNumber of long-polls currently parked for this mailbox, all devices. A client that expects its own poll to be parked and sees 0 knows it has been dropped.
42–432retention_daysRetention window applied to this mailbox’s Tells (30 today).
44–474reservedZero. Reserved for a future version.

How a client uses it

  • At start-up, once per beacon: read commands to choose Wait or Ping; read max_hold_seconds and sweep_seconds to set the socket timeout or the Wait hold; compare server_time with the local clock and log skew above a threshold.
  • On the health refresh (rest_core: every 15 minutes): a successful reply is the “beacon reachable and authenticating” signal the status bar lacks today. A refused connection here is unambiguous, unlike an empty poll.
  • After a long absence: compare newest_publish with the stored cursor. If the backlog is large (retained_count against max_records), the client knows in advance that it must page.
  • Diagnostics: parked_polls shows whether the beacon still holds this client’s connection. A log line pairing the server’s counts with the client’s makes support conversations like the one in bugs/BeaconIssues.txt a matter of reading two numbers.

Status codes

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSPayload follows.
50x05ERROR_INVALID_COMMAND_GROUPServer build has no group 22. The client assumes Ping/Peek only, a 900 s hold and a 60 s sweep.
80x08ERROR_COIN_NOT_FOUNDMailbox (denom, SN) is not loaded on this RAIDA.
160x10ERROR_INVALID_PACKET_LENGTHBody is not 50 bytes.
330x21ERROR_INVALID_EOFTerminator was not 3E 3E.
340x22ERROR_INVALID_ENCRYPTIONBody could not be decrypted or validated.
370x25ERROR_INVALID_CRCChallenge CRC mismatch.
1940xC2ERROR_FILESYSTEMRetention index unreadable. Counts could not be computed.
2000xC8ERROR_INVALID_ANBody AN does not match the stored AN.

Compatibility

  • Existing clients: unaffected; they never send 93.
  • Existing servers: answer 5. The client falls back to compiled-in assumptions, which is what it does today.
  • Rate limiting: the call is cheap (one index lookup, no file reads) but authenticated, so it should sit under the same per-coin limiter as Peek so a misbehaving client cannot use it as a probe.
  • Versioning: new fields go into reserved and bump version; the payload length stays 48 until a version 2 defines a longer one.

Common mistakes

Polling it instead of Wait

Beacon Status tells you whether there is mail; it does not deliver it and does not park. Use it for health and set-up, then long-poll with Wait or Ping.

Treating server_time as the cursor

Ping/Peek cursors are sender timestamps, not server time; Wait cursors are opaque. Use newest_cursor to start Wait “from now”, and use server_time only to measure skew.