Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions plans/2026-08-09-fast-queue-rotation-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Fast queue rotation — implementation plan

Branch: ep/drop-agent-versions. RFC: ../rfcs/2026-08-09-fast-queue-rotation.md.

No new agent message. Each side detects the rotation from the replacement
reference (`dbReplaceQueueId`) on its own new queue.

## New definitions

Agent/Protocol.hs
- Gate on the existing `rpcAddressSMPAgentVersion` (v8). Renaming it to cover both features can be considered separately; no alias.
- `SndSwitchStatus` constructor `SSSecuringQueue` with StrEncoding/JSON/field instances.
- `InternalCommand` constructor `ICQSndSecure SMP.SenderId` with tag.

Agent/Store(.AgentStore)
- `SSSecuringQueue` in `setSndSwitchStatus` mapping.
- `ICQSndSecure` in internal command store/parse.

Agent.hs
- `storeRotationConfirmation` — variant of `storeConfirmation` that stores
`AgentConfirmation {e2eEncryption_ = Nothing, encConnInfo = ""}` with msgType
`AM_CONN_INFO`, `sndMsgPrepData_ = Nothing`, no ratchet step.

## Sender B — Agent.hs

`qAddMsg` (new-queue-sender branch)
- After `addConnSndQueue` for the new snd queue, choose path:
- fast: `connAgentVersion cData' >= rpcAddressSMPAgentVersion && senderCanSecure (queueMode of new queue)`.
- else: current `QKEY` path (unchanged).
- Fast: `enqueueCommand c "" connId (Just newSrv) $ AInternalCommand $ ICQSndSecure sndId`;
`setSndSwitchStatus db sq $ Just SSSecuringQueue`; notify `SWITCH QDSnd SPStarted`.

`ICQSndSecure sId` (new, in runCommandProcessing, model on `ICQSecure`)
- Find new snd queue `sq'` in sqs with `dbReplaceQueueId = Just _`, status `New`.
- `agentSecureSndQueue` (SKEY) on `sq'`; set `Secured`.
- `storeRotationConfirmation c cData sq'`; `submitPendingMsg c sq'`.

runSmpQueueMsgDelivery — success `AM_CONN_INFO`
- Branch on `sq` replacement reference:
- `dbReplaceQueueId = Just _`: rotation. Mirror `AM_QTEST_` — `checkSQSwchStatus sq' SSSecuringQueue`; remove old snd queue; `setSndQueuePrimary`; `deleteConnSndQueue`; notify `SWITCH QDSnd SPCompleted`.
- otherwise: current join completion (`CON`/`setStatus`).

runSmpQueueMsgDelivery — AUTH `AM_CONN_INFO`
- Same branch: rotation → `qError msgId "rotation confirmation: AUTH"`; otherwise current `connError NOT_AVAILABLE`.

## Recipient A — Agent.hs

`smpConfirmation`, `New` case, before the RcvConnection/DuplexConnection split
- If `dbReplaceQueueId rq = Just replacedId` (rq is R'):
- `setRcvQueueConfirmedE2E db rq (C.dh' e2ePubKey e2ePrivKey) (min v phVer)` (empty encConnInfo not decrypted).
- `setRcvQueuePrimary`, `setRcvQueueStatus rq Active`.
- `setRcvSwitchStatus db replaced $ Just RSReceivedMessage`.
- finalize: update connection, notify `SWITCH QDRcv SPCompleted`, delete old queue via `deleteConnQueues` (bounded best-effort), not `ICQDelete`.
- no `CON`/`INFO`.

## Switch state

- A: `RSSendingQADD` → (confirmation on R') → `RSReceivedMessage` → done. `RSSendingQUSE` skipped for fast path.
- B: `SSSecuringQueue` → (confirmation delivered) → done.

## Unchanged

QADD wire format, slow QKEY/QUSE/QTEST path, abort (`canAbortRcvSwitch` still allows through `RSSendingQADD`).

## Tests

- new/new: rotation completes with current server stopped after QADD.
- new/old and old/new: fall back to QKEY path, complete.
- redundancy: two current queues, rotate one, secret established on R' only.
79 changes: 79 additions & 0 deletions rfcs/2026-08-09-fast-queue-rotation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
Proposed: 2026-08-09
Protocol: agent-protocol v8
Diagram: ./diagrams/duplex-messaging/queue-rotation-fast.svg
---

# Fast queue rotation (SKEY)

## Problem

In the current rotation the peer returns `QKEY` to the initiator over the
initiator's current receiving queue. When that queue's server is unavailable the
rotation cannot complete, so a client cannot move away from a failed server.

## Solution

When both agents support v8, the peer secures the new queue itself with `SKEY`,
as in the fast connection handshake, and sends an empty confirmation to it
instead of returning `QKEY`. This replaces the `QKEY` and `QUSE` exchange. The
initiator's current server is used only to delete the old queue at the end, and
that deletion does not block completion.

Roles: A initiates (its receiving queue moves; A receives on R'); B is the peer
(B holds the sending queue to A, and secures R' and sends to it).

Sequence (as drawn in the diagram):

A -> R' : create new queue (messaging mode, SKEY allowed)
A -> S -> B : QADD(R') (over A's sending queue; A's current server untouched)
B -> R' : SKEY (B secures R')
B -> R' : confirmation (empty)
R' -> A : confirmation (A derives R' secret, completes the switch)
A -> R (old) : delete (best effort, skipped if the server is down)
B -> R' : messages

## Confirmation

The confirmation is the only message a recipient can read on a queue that is not
yet secured, and it is what establishes the queue's shared secret. This is why
it, rather than an ordinary message, carries the rotation: the earlier abandoned
attempt sent the securing message as an ordinary message, which required the
secret it was establishing.

The confirmation body is empty: both parties already know each other, so no
profile or reply queue is sent. The body is sealed by the queue's NaCl box
(keyed by the shared secret being established) and is not additionally encrypted
with the double ratchet, so the rotation does not advance the message ratchet.
No ratchet key parameters are included, as in the SKEY handshake.

No new agent message is defined. Both parties already know which queue is being
replaced — the initiator because it created R' to replace a specific queue, the
peer because `QADD` states the replaced sending-queue address — so no marker is
needed on the wire.

## Per-queue secret

R' secret is a fresh Diffie-Hellman between the peer's queue key, sent in the
confirmation header, and the initiator's R' key. It does not depend on any
current queue, so redundancy (several current queues) is unaffected.

## Compatibility

Fast rotation runs only when the connection's agreed agent protocol version is 8
or higher; the peer chooses it. Otherwise the `QKEY`/`QUSE` exchange is used.

new A / new B : fast (QADD, confirmation)
new A / old B : slow (old B returns QKEY; new A keeps the QKEY/QUSE handling)
old A / new B : slow (agreed version below 8; new B returns QKEY)
old A / old B : slow

The recipient does not choose by version; it reacts to whichever message arrives
(`QKEY` or a confirmation on R').

## Dead current server

The initiator completes the switch and stops using the old queue without waiting
for the old queue to be deleted on its server. Deletion is retried a bounded
number of times and then abandoned, so an unavailable current server never
blocks the rotation.
Loading