Introducing Relation ONE IM

Fig. 1-1 IM Architecture
Relation ONE IM is a decentralized private messaging protocol.
Relation ONE IM realizes true decentralization and privacy.
It consists of four parts: (see Fig. 1-1)
  • Client: This is a front-end application for users to encrypt and decrypt messages and to send such messages via Proxy. It uses the standard interface provided by Relation Protocol to manage contacts and groups.
  • Proxy: This is a simple back-end program that accepts messages from "Client" only. It stores the messages to Arweave and pushes them to their receivers.
  • Decentralized Storage(Arweave): To store encrypted messages permanently.
  • Contracts: A set of contracts (Relation Name Service, Follow, DAO) conforming to Relation Protocol to store user information, Follow relationships, and information on DAOs.

1. The main procedure for sending messages

See Fig. 1-1
  1. 1.
    A sender connects to "Client" using a wallet.
  2. 2.
    The "Client" uses Lit Protocol to encrypt the message.
  3. 3.
    "Client" sends the encrypted message to a "Proxy".
  4. 4.
    The "Proxy" stores the message permanently.
  5. 5.
    The "Proxy" sends the message to the receiver's "Client" via Websocket.
  6. 6.
    The receiver decrypts the message on the "Client" using Lit Protocol.

2. Identity

Each pair of Ethereum public and private keys constitutes an identity. Users can use wallets like MetaMask to connect to "Client".
An ethereum address is like an ID for a user. When messaging a user, the message body specifies the receiver's address, and the "Proxy" will push the message to the "Client" logged in using the target address.
The private key signs the message body to ensure the message is sent by that address and cannot be modified by anybody.
The identity is also used to login to Lit Protocol to encrypt or decrypt messages in the "Client".
The identity needs to register a SBT Token in [Relation Name Service](#2.1. Relation Name Service) as a prerequisite for private messaging.

2.1. Relation Name Service

Relation Name Service contract conforms to the Name Service standard published by Relation Protocol.
Before a user can use private messaging, a SBT Token shall be acquired by the user via registering with Relation Name Service contract.
In a chat, we can see other users' information ,such as Name and Avatar, from Relation Name Service.
Relation Name Service contract provides the following interfaces:
  • register: register(address owner, string calldata name, bool reverseRecord) external returns (uint)
  • set an avatar: setProfileURI(string memory profileURI) external
  • Query a user name: nameOf(address addr) external view returns (string memory)
  • Query an avatar: profileURI(address addr) external view returns (string memory)
Fig. 1-2 Relation Name Service

3. Client

"Client" is a front-end application. It can be a Web application, Chrome plugin, desktop or mobile application. In Relation ONE, it is a Chrome plugin.
When users are logged in their "Client"s, the "Client" will connect to Proxy via WebSocket. When someone sends messages to said users, the Proxy will push the messages to "Client", and the "Client" can decrypt the messages using Lit Protocol.
Before sending messages, the "Client" composites Messages according to the [Messages structure](#7.1. Message structure) , encrypts the Messages using Lit. Finally, the encrypted Messages are sent to Proxy.
Data privacy is secured this way because messages are encrypted and decrypted on the "Client" side.
The "Client" also manages [contacts](#5.1. Manage contacts) and [groups](#5.2. Manage groups) by calling respective contracts.

4. Proxy

Relation ONE Proxy is a back-end application which provides a set of REST API (to receive messages from "Client" and to modify states) and a WebSocket interface (to push messages to "Client".)
"Proxy" will store the messages sent from "Client" to Arweave. Because the messages are stored in a decentralized manner, "Client" does not rely on any single "Proxy". Even if one "Proxy" fails, "Client" can change to another "Proxy" with the message data intact.

4.1. Receive/Send Messages

Client sends encrypted messages to Proxy via REST API. The Proxy will store the messages to Arweave according to the Message Persistence strategies and push the messages to the Client logged in by the receiver.
Fig. 1-3 Send Message

4.2. State Storage

Data concerning states, such as unread messages, muted group messages, sticky messages, are stored by a "Proxy" in its own database. These data are not shared among different Proxies when a Client switches between them.

4.3. Message Persistence

[Proxy](#4. Proxy) uploads [Messages](#7.2. Messages) containing one or multiple [Message(s)](#7.3. Payload). When uploading to Arweave, each Transaction has a tag pointing to the Hash of the previous Transaction.
There are three strategies for Message Persistence:
  1. 1.
    Pack and upload the messages to Arweave every 12 hours.
  2. 2.
    If there is no message within 12 hours, then the pack will not be conducted, and the count for "12 hours" restarts.
  3. 3.
    When the messages exceeded 100MB in size, they will be packed and uploaded to Arweave even if the scheduled task is not yet triggered.

4.4. The REST interface

A "Proxy" should provide the following interfaces:
Http Method
Request Body
Response Body
Send messages
Query messages
Mark as read
Unread message count
Chat lists

5. Manage relationships

5.1. Manage contacts

Users manage contacts using Follow contract conforming to Relation Protocol.
Each [identity](#2. Identity) creates a Follow contract using FollowRegister (see Fig. 1-4). When Alice follow Bob, the former needs to call the follow() method of Bob's Follow contract .
Fig. 1-4 Follow
  • Friend list: List all Follow contracts with FollowRegister and list each user's follow information.
  • Follow: call the follow() method from the other person's contract.
  • Unfollow: call the unfollow() method from the other person's contract.

5.2. Manage groups

Group chats are managed using DAO contracts. The addresses listed in the contract which hold Semantic SBTs are considered group members.
"Client" manages groups by calling standard DAO interface.
Example: A creator creates a DAO contract via DaoRegister. The isFreeJoin() method is used to set whether people are free to join the group. With the join() method, one can join a group chat. With the remove(address addr) method, one can leave a group chat. (see Fig. 1-5)
Fig. 1-5 DAO
  • Create a group chat: The Client calls the deployDaoContract(address owner,string memory name) external returns (uint256) method of the DaoRegister contract to create a new group chat.(See Fig. 1-5).
  • Join a group chat: If the isFreeJoin() is set to True, then one can use the join() method to freely join a group. It is set to False, then the administrator needs to add members via the method addMember(address[] memory addr) external.
  • Leave a group chat : One can call the method remove(address addr) to leave a group chat. Or, the administrator can call the method remove(address addr) to remove a member.
  • Set a group announcement: The administrator of a group sends a special [message](#7.1. Message structure) in the group to realize this, with the kind set to 3.
  • Group name: Query a group's name via the method name() .
  • Group avatar: Query the group avatar using the method daoURI().
  • Group members: The addresses listed in the contract holding its SBT Tokens are group members.

6. Chat

6.1. P2P chat

Users can chat with their [contacts](#5.1. Manage contacts) or send messages to Ethereum addresses not owned by their contacts.
A Client(A)encrypts payload of the message using Lit, with accessCondition as the receiver's Ethereum address. The receiver should also be the receiver's Ethereum address. Then, the procedure composites "Messages" according to the [Message Structure](#7.1. Message structure) and sends it to Proxy. The Proxy will push the messages to the Client (B) logged in with the receiver's address.

6.2. Group chat

When sending group messages, a user A logs in a Client(A)and encrypts the message's payload with Lit, with the accessCondition set to be visible to group members.) The receiver should be set to the group chat's contract address. Then, the procedure composites "Messages" according to the [Message Structure](#7.1. Message structure) and sends it to Proxy. The Proxy will then push the messages to each Client of the users in the group, such as Client(C)、Client(D)、Client(E)...

6.3. Encrypt/Decrypt

[Client](#3. Client) implements Lit Protocol SDK. Each message is encrypted with the Client on the sender's side and decrypted with the Client on the receiver's side.
Fig. 1-6 Encrypt and Decrypt messages
As with Fig. 1-6, Alice wants to send an encrypted message to Bob: Hi.
  1. 1.
    Alice's "Client" will use the Lit SDK to encrypt Hi locally. Subsequently, it generates the encrypted message encryptedString and key symmetricKey:
const messageToEncrypt = "Hi";
// 1. Encryption
const { encryptedString, symmetricKey } = await LitJsSdk.encryptString(messageToEncrypt);
  1. 2.
    Alice's "Client" sets accessControlConditions to be visible to Bob's address, and uploads accessControlConditions and key symmetricKey to Lit's node.
const accessControlConditions = [
"contractAddress": "",
"standardContractType": "",
"chain": "ethereum",
"method": "",
"parameters": [
"returnValueTest": {
"comparator": "=",
//Bob's wallet address
"value": "0x50e2dac5e78B5905CB09495547452cEE64426db2"
// 2. Saving the Encrypted Content to the Lit Nodes
const encryptedSymmetricKey = await litNodeClient.saveEncryptionKey({
  1. 3.
    Alice's "Client" composites Message and send it to "Proxy".
const message = {
"sender": "Alice's wallet address",
"receiver": "Bob's wallet address",
"payload": "${encryptedString}",
"encryptedSymmetricKey": "${encryptedSymmetricKey}",
  1. 4.
    The Proxy forwards the message to Bob's "Client".
  2. 5.
    After Bob's "Client" received Message pushed by the "Proxy", it acquires the key _symmetricKey after it passed the verification conducted by the Lit node:
// 5. Decrypt it
// <String> toDecrypt
const toDecrypt = LitJsSdk.uint8arrayToString(encryptedSymmetricKey, 'base16');
// <Uint8Array(32)> _symmetricKey
const _symmetricKey = await litNodeClient.getEncryptionKey({
  1. 6.
    Bob decrypts the message locally:
// <String> decryptedString
let decryptedString;
// gets the original text: Hi
decryptedString = await LitJsSdk.decryptString(

7. Define a message

7.1. Message structure

"id": "${messageId}",
"encryptedBy": "Lit",
"sender": "${senderWalletAddress}",
"receiver": "${receiverWalletAddress/receiverDaoContractAddress}",
"timestamp": ${the message creates timestamp in milliseconds},
"kind": ${kind},
"tags": [
["quoteId", "${messageId of the quoted message}"],
["mentionedUsers", "${The wallet address of the mentioned user}"]
"payload": "${message encrypted by Lit}",
"encryptedSymmetricKey": "${a hex string that LIT will use to decrypt payload as long as you satisfy the conditions}",
"delegationMessage": "Authorize ${address} to delegate signature, Creation timestamp: ${}, Expiry timestamp: ${}",
"delegationSig": "${The signature generated by signing against ${delegationMessage} using private key}",
"sig": "${The signature generated by signing against ${messageId} using private key}"
  • How to generate messageId : Composite a json string with the following format and calculate its Keccak256:
  • kind
    • 0: p2p message。
    • 1: group message。
    • 2: dao message。
    • 3: announcement。
    • 4: hide message。
  • Session Key:
A pair of public and private keys are generated on the Client side as the Session Key. The Session Key is used to composite delegationMessage. Then, the user's private key will be used to sign against delegationMessage, resulting in delegationSig.
When users send a message, they sign against the message using the private key of the Session Key pair, and attach the delegationMessage and delegationSig. With this delegationChain, it can be verified that the message is indeed signed by the said user.
  • How to generate sig:
Sign messageId using the private key of Session Key, and you will have a sig.

7.2. Messages

"Messages" is an array with one or multiple "Message" with the following format:
Messages = [Message,Message,...]

7.3. Payload

  • Text message
// ...
"type": "TEXT",
"payload": "Hi"
// ...
  • CARD message
// ...
"type": "CARD",
"payload": "type:href;action:shareFavorite;p1(Text),p2(Text):,p3(Text):SPACE ID,p4(Text)"
// ...
  • NFT
// ...
"type": "NFT",
"payload": "type:image;action:nftImage;p1(Text),p2(Text):polygon"
// ...
  • Local IMAGE
// ...
"type": "IMAGE",
"payload": "type:image;action:localImage;p1(Text),p2(Text):im/image/nymb5-kqaaa-aaaaj-4thiq-cai/2f3537d1a2474f2fb92cb5f7de2ed70a.png"
// ...
// ...
"type": "EMOJI",
"payload": "type:image;action:customEmoji;p1(Text),p2(Text):im/emoji/lov6e-qqaaa-aaaaj-xykxq-cai/ac1dc2bf6bad48aeb1a502c0a28ee942.jpg"
// ...
  • Batch transfer
// ...
"payload": "0xd84ed2b4deacbad8a568747fea45f3fc7f2d204595101f99f43006712f3ff290::USDT::20.0000::0x969f85053b44d7eCE8108094420Aba45149b7A3a::qzoo5-ryaaa-aaaaj-xs6ma-cai"
//payload: 0xbaf915e778b044dbf62974d17bd2ab8875bccce8bd02c40dfc241d2893755851::USDT::5.0000::aukee-3yaaa-aaaah-qc3ya-cai::4xtgo-viaaa-aaaaj-aavyq-cai,ms5ob-iqaaa-aaaaj-aa2za-cai,kppoo-ozrgz-rtkn3-cgqyq-cai,meohx-szymu-ytezr-ugiza-cai,oazqd-ndeg4-ytqyj-zga2q-cai
//payload: 0x71de31f73f34fc9af20f7ce3b35ec4e56bf25af476f99cb376779761c2fe91fa::USDT::1.0000::aukee-3yaaa-aaaah-qc3ya-cai::7zshb-ryaaa-aaaaj-aag3a-cai
// ...
// ...
"type": "TEXT",
"payload": "It's an announcement"
// ...

7.4. The structure of a Chat

"id": "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
"name": "test-group",
"avatar": "",
"channelType": "GROUP",
"unreadMessageCount": 0,
"type": "TEXT",
"payload": "the message payload",
"lastPostAt": "1665219499000"

8. Message storage

The application uses Arweave to store encrypted messages permanently to ensure reliability and decentralization.
A series of [Message](#7.1. Message structure) is grouped into [Messages](#7.2. Messages) by [Proxy](#4. Proxy) and uploaded to Arweave. Each Transaction has a tag pointing to the Hash of the previous Transaction, so it is easy to find all history messages with the latest Hash.
Messages = [Message, Message, Message]
Fig. 1-7 Message storage

9. Contract

zkSync Era Testnet
Relation Name Service
DAO docs