Drains every outstanding request this library needs the product to send
to its homeserver, or feed to another device -- design doc section 3bis.
An addition to the frozen surface, not a change to it: OlmMachine has
an outbound side (device/one-time key uploads, key queries, key claims,
and the to-device requests that actually carry a shared session key),
and discarding what this returns is the mistake section 3bis is named
for -- a machine that encrypts to nobody and never learns that any of it
happened.
The returned order is significant, and you must preserve it. Send the
requests in the order this returns them -- not "start them in that order
and let them race": each one has to reach your homeserver before the next
is sent, because the server relays them to the other device in the order
it receives them.
That is a real constraint with exactly one source, and it is worth naming
so it is not optimised away. A verification flow's last two messages are a
confirmation and the acknowledgement that closes the flow, and the far
side silently discards an acknowledgement that arrives before the
confirmation it acknowledges. It then waits for one that has already been
sent. The failure is asymmetric -- your side completes and records the
other device as verified, the other side records nothing -- and neither
side is told. Both messages can land in the same batch, from two different
queues inside the library, so a product that pumps on a timer rather than
after every call is the one that meets this.
Resolving them with markRequestSent is a different matter and is
not ordered at all. It is a lookup by id, so mark them in whatever order
the responses come back, and do not wait for request n to be marked
before sending request n+1.
Requests from different batches were never orderable against each other
-- a batch is a snapshot -- and nothing here changes that. What this
function guarantees is that within one batch, the order it returns is the
order the requests were produced in, across both of the places inside the
library they come from.
What that guarantee is worth, stated so it is not read as more. Two
requests this library produced in an order that matters come out in it,
which is the whole point and is what the verification pair needs. Two
requests with no ordering requirement between them may come out in either
order, run to run, and nothing here promises otherwise -- the last
paragraph of this comment is a measured example of exactly that. So:
preserve the order you are given, and do not read meaning into the
relative position of two requests that have none.
Up to and including 0.1.0-rc.2 this comment said the opposite: that the
array was an unordered set and a product must not infer sequencing from
position. That was true of every request the library could then produce,
and it stopped being true when device verification arrived. The sentence
is recorded here rather than deleted because a consumer who read the old
one and built on it has to be able to find out that it changed.
markRequestSent is not the only thing that ends a request's
life. A later call to this function ends some of them too. Four of the
kinds handed out here -- keys_upload, keys_query, keys_claim and
signing_keys_upload -- are evicted the moment a subsequent call hands
out a fresh request of the same kind, whether or not the older one was
ever marked sent. markRequestSent then rejects that older id with
unknown_request.
That is designed, not a defect, and it is worth knowing why, because
unknown_request for an id a product is legitimately holding otherwise
reads as a library bug. The first three describe a standing need
("these keys want uploading", "these users want querying") rather than
one message. matrix-sdk-crypto re-derives that need from current state
on every call, mints a new and uncorrelated id for it, and forgets the id
it handed out last. So once a fresh one exists, the older id names
nothing the machine is still waiting to hear about, and the fresh request
in that same batch carries what the older one was for.
signing_keys_upload is in that group for a different reason and on a
narrower trigger, and it is the one that will actually catch a product
out. Nothing upstream forgets its id; this library re-derives it, and
only when bootstrapCrossSigning is called again. A second bootstrap
publishes the identical three keys, so keeping both entries would hand a
caller two ids for one publication and two rounds of user-interactive
authentication to finish it. An ordinary second drain does not touch
it, because no fresh one exists to evict it; a second bootstrap followed
by a drain does. That matters because this is the one id a product is
meant to hold across a slow loop with a person in the middle of it: it
survives any number of refused attempts, since only success consumes an
entry, and it does not survive being superseded. Do not call
bootstrapCrossSigning again while an authentication loop is in flight.
What a caller must do about it: resolve a batch before drawing the
next. Drain, send in order, markRequestSent each response, and only
then call this again.
Within one batch, marking may overlap sending -- nothing in one batch
evicts another member of it, so request n need not be marked before
request n+1 is sent. The sends themselves stay ordered, which is
the half of this that changed after 0.1.0-rc.2; see the ordering rule
at the top of this comment. This paragraph used to say sending and
marking within a single batch were both safe to do concurrently, which
is the sentence that section retracts.
What is not safe is a second drain overlapping unresolved requests from
an earlier one: two pumps racing, or a drain on a timer alongside a drain
after a write, will produce unknown_request for ids the product still
holds.
On unknown_request for an id from an earlier batch, do not retry it.
Discard the response that id was going to carry and pump again. Nothing
is lost: the need was re-derived rather than dropped, and the request that
supersedes it is either already in hand or waiting in the next drain.
to_device, signature_upload and room_message ids are never
evicted this way, and two keys_query ids escape it as well. The
first three each name one independently deliverable message, so each
stays outstanding until markRequestSent resolves it. The other two are
standing needs that have to outlive an ordinary drain: the out-of-band
query about this account, which only another query of its own kind
evicts, and the query a verification finishing by a scanned code queues,
which nothing evicts at all, because its answer is the entire product of
that verification. Both arrive as keys_query and nothing on this
surface tells them apart from an ordinary one, so a product can hold
two live keys_query ids at once: a newer one is not evidence that an
older one is dead. For every kind,
marking is not optional bookkeeping; it is what advances the underlying
state machine. A product that calls this but never calls
markRequestSent keeps being handed the same requests, including -- for
a to-device request the machine could not yet deliver -- a stale
m.room_key.withheld notice sitting alongside the actual session key, in
no reliable order relative to it (measured across ten runs of the same
sequence: six with the notice first, four with the key first). That is
not a counter-example to the ordering rule above and is the reason it is
scoped as it is: neither of those two requests is order-significant
against the other, they are held keyed by transaction id rather than by
production order, and a transaction id is random. The
measured harm from that specific case is bounded -- that withheld notice
carries no scope and no session id of its own, so it names nothing for a
recipient to act on, and a matrix-sdk-crypto-based recipient's own
add_withheld_info deliberately ignores exactly this notice kind -- but
relying on that is not a substitute for calling markRequestSent: it is
the only thing that stops the duplication at the source.
Drains every outstanding request this library needs the product to send to its homeserver, or feed to another device -- design doc section 3bis. An addition to the frozen surface, not a change to it:
OlmMachinehas an outbound side (device/one-time key uploads, key queries, key claims, and the to-device requests that actually carry a shared session key), and discarding what this returns is the mistake section 3bis is named for -- a machine that encrypts to nobody and never learns that any of it happened.The returned order is significant, and you must preserve it. Send the requests in the order this returns them -- not "start them in that order and let them race": each one has to reach your homeserver before the next is sent, because the server relays them to the other device in the order it receives them.
That is a real constraint with exactly one source, and it is worth naming so it is not optimised away. A verification flow's last two messages are a confirmation and the acknowledgement that closes the flow, and the far side silently discards an acknowledgement that arrives before the confirmation it acknowledges. It then waits for one that has already been sent. The failure is asymmetric -- your side completes and records the other device as verified, the other side records nothing -- and neither side is told. Both messages can land in the same batch, from two different queues inside the library, so a product that pumps on a timer rather than after every call is the one that meets this.
Resolving them with markRequestSent is a different matter and is not ordered at all. It is a lookup by id, so mark them in whatever order the responses come back, and do not wait for request n to be marked before sending request n+1.
Requests from different batches were never orderable against each other -- a batch is a snapshot -- and nothing here changes that. What this function guarantees is that within one batch, the order it returns is the order the requests were produced in, across both of the places inside the library they come from.
What that guarantee is worth, stated so it is not read as more. Two requests this library produced in an order that matters come out in it, which is the whole point and is what the verification pair needs. Two requests with no ordering requirement between them may come out in either order, run to run, and nothing here promises otherwise -- the last paragraph of this comment is a measured example of exactly that. So: preserve the order you are given, and do not read meaning into the relative position of two requests that have none.
Up to and including
0.1.0-rc.2this comment said the opposite: that the array was an unordered set and a product must not infer sequencing from position. That was true of every request the library could then produce, and it stopped being true when device verification arrived. The sentence is recorded here rather than deleted because a consumer who read the old one and built on it has to be able to find out that it changed.markRequestSent is not the only thing that ends a request's life. A later call to this function ends some of them too. Four of the kinds handed out here --
keys_upload,keys_query,keys_claimandsigning_keys_upload-- are evicted the moment a subsequent call hands out a fresh request of the same kind, whether or not the older one was ever marked sent.markRequestSentthen rejects that older id withunknown_request.That is designed, not a defect, and it is worth knowing why, because
unknown_requestfor an id a product is legitimately holding otherwise reads as a library bug. The first three describe a standing need ("these keys want uploading", "these users want querying") rather than one message.matrix-sdk-cryptore-derives that need from current state on every call, mints a new and uncorrelated id for it, and forgets the id it handed out last. So once a fresh one exists, the older id names nothing the machine is still waiting to hear about, and the fresh request in that same batch carries what the older one was for.signing_keys_uploadis in that group for a different reason and on a narrower trigger, and it is the one that will actually catch a product out. Nothing upstream forgets its id; this library re-derives it, and only when bootstrapCrossSigning is called again. A second bootstrap publishes the identical three keys, so keeping both entries would hand a caller two ids for one publication and two rounds of user-interactive authentication to finish it. An ordinary second drain does not touch it, because no fresh one exists to evict it; a second bootstrap followed by a drain does. That matters because this is the one id a product is meant to hold across a slow loop with a person in the middle of it: it survives any number of refused attempts, since only success consumes an entry, and it does not survive being superseded. Do not callbootstrapCrossSigningagain while an authentication loop is in flight.What a caller must do about it: resolve a batch before drawing the next. Drain, send in order,
markRequestSenteach response, and only then call this again.Within one batch, marking may overlap sending -- nothing in one batch evicts another member of it, so request n need not be marked before request n+1 is sent. The sends themselves stay ordered, which is the half of this that changed after
0.1.0-rc.2; see the ordering rule at the top of this comment. This paragraph used to say sending and marking within a single batch were both safe to do concurrently, which is the sentence that section retracts.What is not safe is a second drain overlapping unresolved requests from an earlier one: two pumps racing, or a drain on a timer alongside a drain after a write, will produce
unknown_requestfor ids the product still holds.On
unknown_requestfor an id from an earlier batch, do not retry it. Discard the response that id was going to carry and pump again. Nothing is lost: the need was re-derived rather than dropped, and the request that supersedes it is either already in hand or waiting in the next drain.to_device,signature_uploadandroom_messageids are never evicted this way, and twokeys_queryids escape it as well. The first three each name one independently deliverable message, so each stays outstanding untilmarkRequestSentresolves it. The other two are standing needs that have to outlive an ordinary drain: the out-of-band query about this account, which only another query of its own kind evicts, and the query a verification finishing by a scanned code queues, which nothing evicts at all, because its answer is the entire product of that verification. Both arrive askeys_queryand nothing on this surface tells them apart from an ordinary one, so a product can hold two livekeys_queryids at once: a newer one is not evidence that an older one is dead. For every kind, marking is not optional bookkeeping; it is what advances the underlying state machine. A product that calls this but never callsmarkRequestSentkeeps being handed the same requests, including -- for a to-device request the machine could not yet deliver -- a stalem.room_key.withheldnotice sitting alongside the actual session key, in no reliable order relative to it (measured across ten runs of the same sequence: six with the notice first, four with the key first). That is not a counter-example to the ordering rule above and is the reason it is scoped as it is: neither of those two requests is order-significant against the other, they are held keyed by transaction id rather than by production order, and a transaction id is random. The measured harm from that specific case is bounded -- that withheld notice carries no scope and no session id of its own, so it names nothing for a recipient to act on, and amatrix-sdk-crypto-based recipient's ownadd_withheld_infodeliberately ignores exactly this notice kind -- but relying on that is not a substitute for callingmarkRequestSent: it is the only thing that stops the duplication at the source.