Problem
Currently all the traffic from 1-to-1 messages / group chats / device syncing goes into a single whisper topic.
This makes this topic fairly heavy and effectively prevents us from retrieving more than 24 hours of data.
We have been talking about moving to a partitioned topic, but we have faced the issue on how to keep compatibility once we do.
During the discussion, we reasoned that moving to device-to-device will give us a better way to upgrade our protocol without breaking compatibility, as we will be able to know who is listening on the other side essentially.
Proposed solution
Currently we use already device-to-device communication for group chats, the proposed solution means extending this method for 1-to-1 chats and add some form of topic negotiation on top of it.
Information about devices are published periodically by each device and can be looked up by other devices when sending a message, so they know which devices to target.
Given that user A has 3 devices A1, A2, A3 and user B has 3 devices B1, B2, B3, we would like to negotiate a topic T and have all devices being able to listen to that topic.
We also want to minimize the amount of chatting between devices, as we should consider them mostly offline.
Topic seeding
The fastest way to negotiate a topic would be for whoever starts the conversation to send a random topic to all the other devices, wait for the other devices to acknowledge and then start using it to publish.
Although this methods works, we don’t want to have entropy from a single source, as that might potentially allow some vectors of attack (essentially we are giving A the ability to have B listen to any topic of their choice), so we can leverage the fact that we use key pairs to calculate a shared topic, using diffie-hellman for example:
On A side:
DH(Aprk, Bpk) -> Append some constant bytes -> Sha3 -> Takes the first 4 bytes
On B side:
DH(Apk, Bprk) -> Append some constant bytes -> Sha3 -> Takes the first 4 bytes
The key initially generated can be used for symmetric encryption, and the 4 bytes as topic.
In this case there’s no need to add any extra information to the already existing message as all the information is already known.
So in short, on receiving a message from A, B will simply calculate the topic using A(pk) and listen for messages on the new topic.
Converge
The problem is that we can only stop publishing on the shared topic and move to the new topic only when all the devices participating in the conversation have acknowledged the new topic, otherwise some of the devices will be left behind.
To do this, we can ask devices to send acknowledgement messages on the new topic, and we can have devices carry that information with each message (optionally to save bandwidth, we can compute paddings to check etc), so that latecomers or new devices (who might have missed acknowledgements), will be brought up to date.
For example, say we have A1
A2
B1
B2
:
A1 sends a message to B1, B2 It will also send a message to A2 if paired
B1 receives the message, and acknowledges on topic T, including {:acknowledgements => ['A1
]}`
B2 sees B1 message, acknowledges on topic T, includes {:acknowledgements => ['A1', 'B1']}
And so on.
Once all the devices are on the new topic, we can only publish on the new topic, and we can exploit the fact that we use a symmetric key to send a single message (the content will be encrypted differently for each device though, not to invalidate pfs).
If not everyone is on the same side, we fallback to the shared topic for that user.(i.e. if A2 has not acknowledged T, we will send a message to A on the shared topic, targeting A1 & A2, as it is now), the pairing message to B2 can be sent on the new topic, provided that B2 has acknowledged.
If a new device joins in (say B3
), there are two cases:
- User
B
pairs the device and syncs the chat.
In such caseB3
will generate the topic for the chat, fetch historical messages and might see some of the messages/acknowledgement, and will be brought up to date. - User
B
does not pair the device.
Eventually user A will detect the new device, at which point the next message will be sent on the shared topic, with acknowledgement informations included, untilB3
acknowledges on topicT
, after which only topicT
will be used.
There are quite a few variations on this theme ( have new device ask for acknowledgements, different way to calculate the topic etc), but the basic idea is:
- We move to device-to-device communication
- We have a deterministic secret topic based on the two users key pairs
- We wait for all the devices to acknowledge the new channel until we fully move to the new topic
- We have devices acknowledge automatically on the new topic to speed up the process, and we possibly carry that information on other messages in order for new devices to catch up.
Because moving to a new topic can take quite a long time (we need all the devices to acknowledge, or to be considered stale, which will happen after we haven’t seen that device for x days), we also might want to move to a partitioned topic.t
The method used for that is essentially similar, by versioning the bundles containing device information https://github.com/status-im/status-go/blob/21f9f09586bb36cbe55f2895aeff820aa4ffd705/services/shhext/chat/encryption.proto#L11 we know which ones have moved to the new topic and we can target accordingly (in this case we don’t need acknowledgements as we already publish this information periodically).
Eventually this should decrease the bandwitdth on the shared topic as more people upgrade, and give us a way to better handle compatibility issues.
It is worth mentioning though that this is a breaking change in a way, as we are fundamentally changing the way we currently send messages, and we can only target devices from version 0.9.32 (although we can avoid breaking compatibility if we don’t detect any device, so it will break if a user is running a mix of >= 0.9.32 and < 0.9.32, but not if it’s running only < 0.9.32).
Let me know what you think, feedback is welcome, also let me know if something needs to be better described (these are not specs though, just a description of a possible solution).