Description
PR is at #6404
Existing group consistency algorithm is built in ad-hoc way by fixing known cases where it practically failed and adding the tests each time. At this moment there is one unfixed case in the form of the PR #6021
Instead of fixing this case one more time with another patch, current plan is to rebuild group consistency from scratch. Algorithm is currently defined at https://github.com/chatmail/specs/tree/main/group-membership with a TLA+ model that was used to reject simpler solutions by finding counter-examples for "eventual consistency" property. There is a Python model of the algorithm at https://github.com/chatmail/specs/tree/main/gmc which shows that #6021 case is fixed and also tests the "immediate consistency" property. We will also keep all existing tests already defined in the core codebase, they should pass and maybe changed if there is a good reason, but not removed, especially when they are testing compatibility with older Delta Chat and MUAs that is not captured by the formal spec.
The idea of the new algorithm is to maintain group member set as a Last-Write-Wins set CRDT. To determine if some operation happened earlier or later we are going to use message timestamps with second precision. Using logical clocks (e.g. vector clocks) was considered but sending them around would increase the amount of data and the result of having temporarily unsynchronized clock between devices is not fatal. Nowadays most of the devices are mobile devices that have clocks synchronized over the network without users having to know about it, I personally don't remember having to adjust my clock on any of the devices in the last 5-10 years at least, it just works and we don't need high resolution.
Currently chats_contacts
table consists of two columns chat_id
and contact_id
. We will also add add_timestamp
and remove_timestamp
columns that default to 0. If add_timestamp >= remove_timestamp
, the member is actually in the chat, otherwise the row is a tombstone. Note that if add_timestamp == remove_timestamp
, member is considered to be part of the chat as it simplifies database migration.
We also add new headers Chat-Group-Past-Members
and and Chat-Group-Member-Timestamps
. Chat-Group-Past-Members
contains a list of past group members in a format similar to the To:
header.
Chat-Group-Member-Timestamps
contains space-separated (just like the To
field for easy wrapping)
unix timestamps (in seconds, just integers) for the To
field followed by Chat-Group-Past-Members
field in exactly this order. I have not decided yet if it makes sense to include the timestamp of the sender, i.e. for the From
field. When member leaves the group, self address is anyway included in the Chat-Group-Past-Members
. We also want to have sealed sender at some point. I will leave the sender timestamp out for now. If this becomes a problem, we can add the sender explicitly in the To
field, this is probably a cleaner solution.
There is no special compression for timestamps, messages are compressed by OpenPGP anyway. There are some ideas to e.g. only send differences for timestamps other than the first one or hex-encode, but I am not going to do this for the first implementation. We can play with this late before merging.
When a message is received, if there is a Chat-Version
header and Chat-Group-Member-Timestamps
header
exists, do the merging. First, adjust the received timestamps so they are not in the future by taking the minimum of the current timestamp and the received timestamp. Same when loading the timestamps from the database, if they are in the future assume them to be the current timestamp.
For every member that does not exist in the chats_contacts
table yet, just accept the state from the received message and the new timestamp. If the member already exists in the chats_contacts
table, accept the new state if the received timestamp is higher than stored timestamp. In case of the same timestamp and conflict, e.g. local state says the member is removed (is_tombstone
is true) and received state says the member is added (it is in the To:
field rather than Past-Members:
field), prefer adding the member. Adding members is preferred to removing members because in this case added member eventually learns that it is still part of the group and can leave while if the user is removed they stop receiving messages and may not notice that they are no longer in the group.
If the message is received with Chat-Version
but without Chat-Group-Member-Timestamps
, it is an old Delta Chat message. In this case if there is a Chat-Member-Added
or Chat-Member-Removed
header, do the action locally if the message is newer than the timestamp stored locally.
If the message is received without Chat-Version
header, it is a message sent from non-Delta Chat client. In this case treat all members in the To
field as if they are added with timestamp 0. This allows adding members to the group
with non-DC clients, but not re-adding or removing them. It is mostly to support ad-hoc groups which are actually email threads and normally don't live long enough to have several iterations of member removals and re-additions.
Expiration for tombstones is moved into #6427