Hello,
we are revamping the group chat protocol and it would be good to get some feedback before we pull the trigger.
I am going to describe the current idea being implemented so that and hopefully I can get some constructive feedback. This is not related to encryption as this is handled at a layer above, only deals with group membership changes.
basic idea
Basically the idea is that the problem is simply “how to keep a piece of state (group membership) synchronized across peers?”
To solve the problem we can use techniques used before to synchronize state: we will build the state from a set of events that are propagated among peers.
Because there’s no acknowledgement between peers, we don’t know that the message has been delivered, so group membership updates are exchanged with each message.
That is necessary to avoid relaying on the delivery of a single message for communication to happen.
Group membership updates are propagated also among peers who haven’t published updates themselves, for example A publishes an update / B receives the updated, B will propagate A update in the next message (potentially with their own update, if any change in membership originated).
format
The format of the message is:
{
"chat-id": "string",
"updates": [ {
"signature": "signature-containing-chat-id-and-hash-of-events",
"clock-value": 1, // We need to be able to order events if originating from different peers
"events": [
{"type": "name-changed", "name": "chat-name"},
{"type": "member-added", "member": "public-key"},
{"type": "admin-added", "member": "public-key"}
]
},
]
}
operations
Upon receiving a message, the peer will:
- Validate the signature by building an hash of the events + clock-value + chat-id, to make sure the event cannot be re-used/re-ordered in different chats.
- If validation is successful it will merge the events with the local events and apply those events and build a project of the chat membership, which will then be used.
- If any change in membership has happened, a system message is produced and displayed in the chat (“Blah has joined”, etc). This messages are local to the peer and they might not be the same among them (out of order messages/messages being dropped), they represent the moment things have changed for the local peer, not for the whole group.
type of events
The type of events are:
name-changed
: The name of the chat has been changed
member-added
: A member has been added
member-removed
: A member has been removed
admin-added
: A member has been granted admin privileges
admin-removed
: An admin has forfeited admin privileges
rules
We need a set of rules to apply those events so that changes are authorized and the resulting state is the same across peers (conflict free).
- Events are totally ordered by clock-values, ties are broken by event-type/public-key
- Only admins can issue a
name-changed
event - Only admins can issue a
add-member
event - Only admins can issue a
add-admin
event - Admins can issue a
remove-member
event on members (not on admins), members can issue aremove-member
on themselves. - Only admins can issue a
remove-admin
event, and they can only remove themselves. add-admin
on a non-existing member, invites the member in the chat has well- An admin cannot forfeit their role if no other admin is in the chat
Basically we want to avoid having power struggles between admins, so once a user is made an admin only they can forfeit willingly their role, at which point they can leave the chat (or be removed by another admin), issuing a remove member on themselves.
Rule 7 is needed to avoid giving power to admin to veto
another admin, by issuing a back-dated remove-member
after they saw an add-admin
event.
observations
currently the whole chat history/membership is naively propagated with each message, that would only be necessary for when new users join, theoretically we can avoid propagating the history when we know that it has been successfully delivered (I can send just the delta). This optimization does not require a protocol change (just a matter of publishing the delta event instead of the history).
Events can be compacted to further optimize and avoid long histories, this can be done once we are sure no out-of-order messages are still in flight. (thinking about using a plural version of each event from the start {type: 'members-added', members: []}
We use public key signatures to authenticate messages, that basically invalidates plausible deniability on group membership, but given that currently all messages are signed and we have no PD, it won’t change the state of things. Adding PD is technically difficult and cumbersome so not the right moment.
extending the protocol
Invalid events are currently silently ignored, so new events can be added and processed only by peers who understand them.
Other options explored
- Sending always the current state versioned:
This is problematic as it’s difficult to resolve conflicts (remove/added of members) and to make it a conflict-free replicated data structure we’d have to use probably OR sets. It was the initial plan, but seems less flexible. - Send commands, do not propagate
This is what we had before, the trouble is that it does not work well with dropped messages/or out of order messages
Thoughts, ideas, concerns?