Relation ONE IM 介绍

Relation ONE IM是一个去中心化的隐私聊天协议。
实现了Relation Protocol,通过Relation Name Service来管理身份,通过Relationship来管理好友和群组。
使用Lit Protocol来进行消息加密和解密。
使用Arweave来做去中心化存储。
Relation ONE IM真正实现了去中心化和隐私性。
它主要有四部分组成(见图 1-1)
Client: 是一个前端应用程序,帮助用户加密/解密消息、发送消息至Proxy。并且需要调用Relation Protocol的标准接口来进行好友和群组管理。
Proxy: 是一个简单的后端程序,仅用于接收Client发来的消息,存储消息至Arweave并且推送至消息接收者。
Decentralized Storage(Arweave): 用于永久化存储加密的消息。
合约:一系列符合Relation Protocol的合约(Relation Name Service、Follow、DAO),用于存储用户信息、Follow信息、DAO信息。
1. 消息发送主要流程
流程见图 1-1
发消息的用户使用钱包连接至Client
在Client端使用Lit Protocol对消息进行加密
Client将加密消息发生至Proxy
Proxy将消息保存至永久化存储
Proxy通过websocket将消息推送至需要接收消息的Client
收消息用户在Client端使用Lit Protocol对消息进行解密
2. 身份
每一个以太坊的公私钥对就是一个身份。可以使用MetaMask等钱包来连接Client。
以太坊地址相当于用户的id,聊天的时候在消息体中指定接收人的地址,Proxy会将消息推送至目标地址登录的Client。
私钥用于对消息体进行签名,保证了消息是由该地址发出去的,并且不能被任何人修改。
该身份也用于登录Lit Protocol,用来在Client中加密或解密消息。
该身份需要在Relation Name Service注册一个SBT Token作为隐私聊天的准入条件。
2.1. Relation Name Service
Relation Name Service是由Relation部署的一个符合Relation Protocol的Name Service标准的合约。
Relation Name Service作为隐私聊天的准入条件,使用隐私聊天之前需要在Relation Name Service
合约注册,用户将会获得一个SBT Token。
在聊天时,从Relation Name Service
合约中获取对方的Name和头像等信息,可以更方便的我们找到和认出好友。
Relation Name Service合约主要提供以下接口:
注册:
register(address owner, string calldata name, bool reverseRecord) external returns (uint)
设置头像:
setProfileURI(string memory profileURI) external
获取用户名:
nameOf(address addr) external view returns (string memory)
获取头像:
profileURI(address addr) external view returns (string memory)

3. Client
Client是一个前端应用程序,可以是web应用、Chrome插件、桌面应用程序,移动应用程序等,在Relation ONE中是一个Chrome 插件。
用户在登录到Client后,Client会通过WebSocket连接至Proxy。当有人给自己发消息时,Proxy主动会推送消息到Client,Client使用Lit对消息解密后就可以看到明文了。
用户在发消息前,Client按照消息结构组装消息体,使用Lit对消息体进行加密后再发送至Proxy。
由于消息是在Client进行加密和解密的,保证了数据的隐私性。
4. Proxy
Relation ONE Proxy是一个后端应用程序,提供了一套REST Api接口(用于接收Client发送的消息、修改状态等)和一个WebSocket端口(用于推送消息至Client)。
Proxy会将Client发过来的消息存储至Arweave,由于所有消息都记录在去中心化存储中,因此Client并不是强依赖于某一个Proxy。即使Proxy宕机,Client在更换Proxy后,消息数据依然不会丢失。
4.1. 接收/发送消息
Client通过REST Api接口将(加密)消息发送至Proxy,Proxy会根据永久化存储策略将消息存储至Arweave,同时通过WebSocket将消息推送至接收者登录的Client上。

4.2. 状态保存
未读消息数、屏蔽群消息、置顶消息等状态信息,由Proxy记录在自己的数据库中,用户连接到不同的Proxy时未读消息数不共享。
4.3. 消息永久化存储
Proxy会将消息打包上传至Arweave,每个消息包里包含1条或多条消息。每个包上传至Arweave时,都会打一个tag,指向前一个交易的Hash。
永久化存储消息有三个策略:
每12小时将消息打包上传到Arweave上。
如果12小时内没有消息,将不会打包,然后重新计时12小时。
如果没有到定时任务触发的时候,消息的大小超过了100MB,立即将消息打包上传到Arweave。
4.4. REST接口
Proxy需提供以下接口
5. 关系管理
5.1. 好友管理
使用符合Relation Protocol的Follow合约来管理好友。
每个身份都会通过FollowRegister
创建一个Follow合约(见图 1-4),当Alicefollow
Bob的时候,只用调用Bob的Follow合约
的follow()
方法即可。

好友列表:通过
FollowRegister
遍历出所有的Follow合约,再遍历出每个用户的follow信息。关注:调用对方合约的
follow()
方法。取关:调用对方合约的
unfollow()
方法。
5.2. 群组管理
使用符合Relation Protocol的DAO合约来管理群聊,该合约中所有持有Semantic SBT的地址即为群成员。
Client通过调用标准的DAO接口来进行成员管理。
例如:发起人通过DaoRegister
创建一个DAO合约。通过isFreeJoin()
方法设置群聊是否可以自由加入。通过调用join()
方法来加入群聊,通过调用remove(address addr)
方法离开群聊。(见图1-5)

创建群聊:在Client端调用
DaoRegister
合约的deployDaoContract(address owner,string memory name) external returns (uint256)
方法创建一个新的群聊(见图 1-5)。加入群聊:如果
isFreeJoin()
设置为True
。可以通过join()
方法自由加入;如果为False
需要管理员通过addMember(address[] memory addr) external
来添加成员。离开群聊:自己调用
remove(address addr)
方法离开群聊。或者管理员调用remove(address addr)
方法移除某个成员。设置群公告:群公告由群主向群里发送一条特殊的消息,发送时将
kind
设置为3
。群名称:通过
name()
方法获取群名称。群头像:通过
daoURI()
方法获取群头像。群成员:合约里所有持有SBT Token的地址即为群成员。
6. 聊天
6.1. 点对点聊天
可以与好友进行聊天,也可以给非好友的以太坊地址发送消息。具体步骤:
Client(A)使用Lit对消息的payload
进行加密(accessCondition为接收方的以太坊地址),receiver
设置为对方的以太坊地址,按照消息结构组装成消息体后发送至Proxy。Proxy会将消息推送至接收方地址登录的Client(B)。
6.2. 群聊
发送群消息时,用户A登录到Client(A)设置Lit对消息的payload
进行加密(accessCondition为群成员可见),receiver
设置为群聊的合约地址,按照消息结构组装成消息体后发送至Proxy。Proxy会将消息推送至每个群成员所在的Client(C)、Client(D)、Client(E)……
6.3. 加密/解密
Client会集成Lit Protocol SDK,每条消息都在发送者的CLient加密,在接收者的Client解密。

如图 1-6,Alice
要给Bob
发送一条加密消息:Hi
。
Alice的Client会使用Lit SDK在本地对
Hi
进行加密,得到消息密文encryptedString
和秘钥symmetricKey
:
const messageToEncrypt = "Hi";
// 1. Encryption
const { encryptedString, symmetricKey } = await LitJsSdk.encryptString(messageToEncrypt);
Alice的Client设置
accessControlConditions
为Bob的地址可见,并将accessControlConditions
和秘钥symmetricKey
上传到Lit的节点
const accessControlConditions = [
{
"contractAddress": "",
"standardContractType": "",
"chain": "ethereum",
"method": "",
"parameters": [
":userAddress",
],
"returnValueTest": {
"comparator": "=",
//Bob's wallet address
"value": "0x50e2dac5e78B5905CB09495547452cEE64426db2"
}
}
];
// 2. Saving the Encrypted Content to the Lit Nodes
const encryptedSymmetricKey = await litNodeClient.saveEncryptionKey({
accessControlConditions,
symmetricKey,
authSig,
chain,
});
Alice的Client组装
Message
,并发送至Proxy。
const message = {
...
"sender": "Alice's wallet address",
"receiver": "Bob's wallet address",
"payload": "${encryptedString}",
"encryptedSymmetricKey": "${encryptedSymmetricKey}",
...
}
Proxy将消息转发至Bob的Client。
Bob接收到Proxy推送的
Message
后,去Lit节点校验通过后拿到加密秘钥_symmetricKey
:
// 5. Decrypt it
// <String> toDecrypt
const toDecrypt = LitJsSdk.uint8arrayToString(encryptedSymmetricKey, 'base16');
// <Uint8Array(32)> _symmetricKey
const _symmetricKey = await litNodeClient.getEncryptionKey({
accessControlConditions,
toDecrypt,
chain,
authSig
})
Bob在本地对消息解密:
// <String> decryptedString
let decryptedString;
try{
// 得到原文:Hi
decryptedString = await LitJsSdk.decryptString(
encryptedString,
_symmetricKey
);
}catch(e){
console.log(e);
}
7. 消息定义
7.1. Message结构
{
"id": "${messageId}",
"encryptedBy": "Lit",
"sender": "${senderWalletAddress}",
"receiver": "${receiverWalletAddress/receiverDaoContractAddress}",
"timestamp": ${the message create timestamp in millisecond},
"kind": ${kind},
"tags": [
["quoteId", "${messageId of the quoted message}"],
["mentionedUsers", "${The wallet address of the mentioned user}"]
],
"type": "${TEXT/CARD/NFT/IMAGE/EMOJI/BATCH_TRANSFER/ANNOUNCEMENT}",
"payload": "${message encrypted by Lit}",
"encryptedSymmetricKey": "${a hex string that LIT will use to decrypt payload as long as you satisfy the conditions}",
"delegationMessage": "授权 ${地址} 代理签名,创建时间:${}, 过期时间:${}",
"delegationSig": "${私钥对${delegationMessage}的签名}",
"sig": "${授权私钥对${messageId}的签名}"
}
messageId
生成方法: 组装下面格式的json字符串,计算其Keccak256
[
<encryptedBy>,
<sender>,
<receiver>,
<timestamp>,
<kind>,
<tags>,
<payload>,
<delegationMessage>,
<delegationSig>
]
kind
0: p2p message。
1: group message。
2: dao message。
3: announcement。
4: hide message。
Session Key:
在客户端生成一个公私钥对作为Session Key,把Session Key的公钥组装成delegationMessage
,使用用户的私钥对delegationMessage
进行签名,得到delegationSig
。
用户发消息时,使用Session Key的私钥对消息进行签名,并且带上delegationMessage
和delegationSig
,通过这个delegationChain,就能验证消息是由用户本人签名的。
sig
生成方法:
通过Session Key的私钥对messageId
进行签名即得到sig
。
7.2. 消息包
1条或多条Message组成的数组称为一个消息包(Messages),它是以下格式:
Messages = [Message,Message,...]
7.3. Payload
文本消息
{
// ...
"type": "TEXT",
"payload": "Hi"
// ...
}
卡片消息
{
// ...
"type": "CARD",
"payload": "type:href;action:shareFavorite;p1(Text):https%3A%2F%2Fspace.id%2Fvoyage,p2(Text):,p3(Text):SPACE ID,p4(Text):https%3A%2F%2Fspace.id%2Ffavicon.ico"
//type:${href};action:${shareFavorite};p1(Text):${URL},p2(Text):,p3(Text):${title},p4(Text):${icon}
// ...
}
NFT
{
// ...
"type": "NFT",
"payload": "type:image;action:nftImage;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fnft%2Fpolygon%2F0x4b5c095380e8e5f016a70a28af570c3bca93b811%2F25908.jpeg,p2(Text):polygon"
//type:image;action:nftImage;p1(Text):${imageURL},p2(Text):${chainName}
// ...
}
本地图片
{
// ...
"type": "IMAGE",
"payload": "type:image;action:localImage;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fim%2Fimage%2Fnymb5-kqaaa-aaaaj-4thiq-cai%2F2f3537d1a2474f2fb92cb5f7de2ed70a.png,p2(Text):im/image/nymb5-kqaaa-aaaaj-4thiq-cai/2f3537d1a2474f2fb92cb5f7de2ed70a.png"
//type:image;action:localImage;p1(Text):${imageURL},p2(Text):${s3ObjectKey}
// ...
}
表情包
{
// ...
"type": "EMOJI",
"payload": "type:image;action:customEmoji;p1(Text):https%3A%2F%2F3fypb-gqaaa-aaaag-aaedq-cai.ic1.io%2Fim%2Femoji%2Flov6e-qqaaa-aaaaj-xykxq-cai%2Fac1dc2bf6bad48aeb1a502c0a28ee942.jpg,p2(Text):im/emoji/lov6e-qqaaa-aaaaj-xykxq-cai/ac1dc2bf6bad48aeb1a502c0a28ee942.jpg"
//type:image;action:customEmoji;p1(Text):${imageURL},p2(Text):${s3ObjectKey}
// ...
}
群转账
{
// ...
"type": "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. Chat结构
{
"id": "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
"name": "test-group",
"avatar": "",
"channelType": "GROUP",
"unreadMessageCount": 0,
"type": "TEXT",
"payload": "the message payload",
"lastPostAt": "1665219499000"
}
8. 消息存储
使用永久存储Arweave来存储加密消息,保证了数据的可靠性和去中心化。
Message由Proxy打包成Messages,上传至rweave上。每个Transaction都被打上一个tag,指向前一个Transaction的Hash。可以方便的根据最新Hash找到所有历史消息。
Messages = [Message, Message, Message]

9. 合约
Relation Name Service
0x04C23dDfE813d8D208bc8120b73F8EF30f423850
0xe4AA1f1E5be0A4E7F80CAC26C2Db1611C3E70c41
-
FollowRegister
0xA0cECa90d7A2033f5162dc73391cdB72272456E7
-
0x734dE4f1b3a1c0619ECB80e3a676a6cFabe72Adc
DaoRegister
0x153C3eF051C7665c9A9c1a718bF12bC8EE6b5115
-
0x7229Dc0117E8484D61137B7304b02b163beC912c
Last updated