Skip to content

Commit c6bd51a

Browse files
author
samuel-videau
committed
feat: LSP19 Social Registry
1 parent 56a264e commit c6bd51a

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

LSPs/LSP-19-Social-Registry.md

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
---
2+
lip: 19
3+
title: Social Registry
4+
status: Draft
5+
type: LSP
6+
author: Samuel Videau <samuel@dropps.io>, António Pedro <antonio@dropps.io>
7+
created: 2022-07-26
8+
updated: 2023-01-01
9+
requires: ERC725Y, LSP2
10+
---
11+
12+
## Simple Summary
13+
14+
This standard describes a smart contract, and a data model to store Social Media information such as posts, likes and follows.
15+
16+
## Abstract
17+
18+
This standard defines a set of data formats and a key-value pair to create a Social Media Feed, combining [ERC725Account](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-0-ERC725Account.md) and an open distributed storage network such as [IPFS](https://ipfs.tech/) or [ARWEAVE](https://arweave.org).
19+
It also defines a smart contract used to guaranty authenticity and timestamp of a post.
20+
21+
## Motivation
22+
23+
Real interoperability requires social media itself to be separated from social media companies. This proposal aims to create a common interoperable standard in which messages generated on one social media app could be transported and read in any other application.
24+
25+
Using a standardized data model to store social media makes content platform-independent and allows it to be read and stored easily. This content can be added to an [ERC725Account](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-0-ERC725Account.md), giving it a Social Media Account character.
26+
27+
## Specification
28+
29+
### LSP19SocialRegistry
30+
31+
A Universal Profile's Social Media State will live under a record referenced by the "LSP19SocialRegistry" data key of their ERC725Y store.
32+
33+
The JSON Url stored inside points to a JSON file that lists all the social media actions of a profile, including posts, likes, dislikes and follows.
34+
35+
```json
36+
{
37+
"name": "LSP19SocialRegistry",
38+
"key": "0xaa093cc8e40d9473239c9fefe0f7e73ad8b9fb1bfca176dcaf5a8af4eacfb1f4",
39+
"keyType": "Singleton",
40+
"valueType": "bytes",
41+
"valueContent": "JSONURL"
42+
}
43+
```
44+
45+
This registry should be updated everytime a new post is added by the user.
46+
Note: It might not be necessary to update the registry for each follow, like or dislike. A balanced trade-off between interoperability/decentralization and cost/UX could be found.
47+
48+
The linked JSON file SHOULD have the following format:
49+
50+
```js
51+
{
52+
"LSP19SocialRegistry": {
53+
"posts": [ // Messages authored by the profile. Includes original posts, comments and reposts.
54+
{
55+
"url": "String", // The url in decentralized storage with the post content and metadata
56+
"hash": "Bytes32" // The hash of the post object
57+
},
58+
...
59+
],
60+
"follows": [ "Address", ... ], // UPs this account has subscribed. Will compose the account's feed.
61+
"likes": [
62+
{
63+
"url": "String", // The url in decentralized storage with the post content and metadata
64+
"hash": "Bytes32" // The hash of the post object
65+
}
66+
], // The identifier (hash) of all the posts this account has liked,
67+
"dislikes": [
68+
{
69+
"url": "String", // The url in decentralized storage with the post content and metadata
70+
"hash": "Bytes32" // The hash of the post object
71+
}
72+
], // The identifier (hash) of all the posts this account has disliked
73+
}
74+
}
75+
```
76+
77+
Below is an example of a social registry:
78+
79+
```JSON
80+
{
81+
"posts": [
82+
{
83+
"url": "ar://NUb9WJ9BbbxLfIDgGwq4zPECBz_df0CrhBmRsYsn8-Y",
84+
"hash": "0xb1029df66ea5ae5cdcc0e84b6e048e37b3df14a4aec92fc4c23d86f8c62e4a4c"
85+
}
86+
],
87+
"likes": [
88+
{
89+
"url": "ar://oHJwvoggzfUxv2WpJIeIlLNC-OR1X1CmGO2zp7BeRgk",
90+
"hash": "0x50f7488034e24cf441d5d02a174d5f56930dbbdbb8815dbb54346be1c5648377"
91+
}
92+
],
93+
"dislikes": [
94+
{
95+
"url": "ar://PPJDYJf6AgZZYKOXZtBBgmdz_-XFCFaBiTuh7Mojecc",
96+
"hash": "0xe15aaaa78c05fc9f9f6d9099db9e984dffe7a732e4fdd503187ad1a91f8390c7"
97+
}
98+
],
99+
"follows": [
100+
"0x53529E4164E5CCA7d6A1C55f8500A57D0F435bee"
101+
]
102+
}
103+
```
104+
105+
### Profile Posts
106+
107+
A Profile Post can be an original message, a comment on another post or a repost. The JSON file should have the following format:
108+
109+
```js
110+
{
111+
"LSP19ProfilePost": {
112+
"version": "0.0.1", // The Metadata version of this post
113+
"author": "Address", // The Universal Profile who authored the post
114+
"locale": "string", // language code - Country Code (de_DE)
115+
"app": "string", // The platform that originated this post
116+
"validator": "Address", // Address of a validator smart contract which will authenticate a post and provide its publication date (more info bellow)
117+
"nonce": "string", // Random value to allow duplicates
118+
"message": "string", // The post original content
119+
"links": [
120+
{
121+
"title": "string", // The link's label
122+
"url": "string"
123+
},
124+
...
125+
],
126+
"tags": [ // Tags attached to a post
127+
"string",
128+
...
129+
],
130+
"medias": [ // Medias attached to a post
131+
{
132+
"hashFunction": "keccak256(bytes)",
133+
"hash": "string",
134+
"url": "string",
135+
"fileType": "string"
136+
}
137+
],
138+
"assets": [
139+
"interface": "string" // Contract interface
140+
"contract": "Address", // Address of the asset contract
141+
"tokenId": "any" // Or null
142+
]
143+
"parentHash": "string", // or null. A post with a parent is a comment
144+
"childHash": "string", // or null. A post with a child is a repost
145+
},
146+
"LSP19ProfilePostHash": {// Hash of the LSP19ProfilePost object
147+
"hashFunction": 'keccak256(bytes)',
148+
"hash": "string",
149+
},
150+
"LSP19ProfilePostEOASignature": "string" // or null
151+
}
152+
```
153+
Below is an example of a post object:
154+
155+
```JSON
156+
{
157+
"LSP19ProfilePost": {
158+
"version":"0.0.1",
159+
"message": "This is the first Lookso post.",
160+
"author": "0x742242E9572cEa7d3094352472d8463B0a488b80",
161+
"app": "Lookso",
162+
"locale": "en-US",
163+
"validator": "0x049bAfA4bF69bCf6FcB7246409bc92a43f0a7264",
164+
"nonce": "415665014",
165+
"links": [
166+
{
167+
"title": "Our website",
168+
"url": "https://dropps.io"
169+
}
170+
],
171+
"medias":
172+
[
173+
{
174+
"hashFunction": "keccak256(bytes)",
175+
"hash": "0x813a0027c9201ccdec5324aa32ddf0e8b9400479662b6f243500a42f2f85d2eb",
176+
"url": "ar://gkmVUoHE4Ay6ScIlgV4E7Fs1m13LfpAXSuwuRGRQbeA",
177+
"fileType": "jpg"
178+
}
179+
],
180+
"assets":
181+
[
182+
{
183+
"interface": "0x622e7a01",
184+
"contract": "0x8cE5Aa1F67FbC9034720E7C9e1e1a841C46faC22",
185+
"tokenId": "0x715f248956de7ce65e94d9d836bfead479f7e70d69b718d47bfe7b00e05b4fe4"
186+
},
187+
{
188+
"interface": "0xda1f85e4",
189+
"contract": "0xbC595d500b30aeb9b04e4D4360f84FdCb2910393"
190+
}
191+
],
192+
"parentHash":"0xdc1812e317c6cf84760d59bda99517de5b5c5190fcf820713075430337805340",
193+
"childHash":""
194+
},
195+
"LSP19ProfilePostHash": {
196+
"hashFunction": "keccak256(utf-8)",
197+
"hash": "0x0017eb3f3b2c10c3387c710e849c64527ae331bfb2d42fb70fbe95588ff5d6cd"
198+
},
199+
"LSP19ProfilePostSignature": "0x2845551019619d59657b6e485d1cb2067479a5bc364270030d7c4143b4cc0ee5279432bee8425f17d091f067e6b8f987390900b1fd82bef52fcb4c8b2b06ab901b"
200+
}
201+
```
202+
203+
The post content and metadata is stored under _LSP19ProfilePost_. The content and metadata are hashed, and the hash is saved under _LSP19ProfilePostHash_. Finally, the controller address is requested to sign the _LSP19ProfilePost_ object. This signature can be obtained, for example, using `web3.eth.accounts.sign(data, privateKey);`
204+
205+
Let's breakdown the _LSP19ProfilePost_ attributes:
206+
207+
* **version** allows clients that adhere to the protocol to display posts according to their version
208+
* **message** is the actual content of a post that will be displayed as text.
209+
* **author** is the address of the Universal Profile that submitted the post.
210+
* **app** is the name of the URL of the platform that originated the post.
211+
* **locale** is the language code used in the post message.
212+
* **validator** is the address of the post validator, the contract that timestamped this particular post. Use it to verify the post authenticity and timestamp.
213+
* **nonce** is what makes a post unique. Otherwise, posts written by the same author with the same message would generate the same hash and collide in the validator storage. The transaction would then revert when someone tried posting the same content twice. Even if on different dates! We don't want that. Anyone has the right to just pass by and say "Goodmorning!" everyday.
214+
* **links** they can be used in the future to extend the standard.
215+
* **tags** they can be used in the future as hashtags.
216+
* **medias** Media files attached to the post. Images, videos, or any other file type.
217+
* **assets** Digital assets attached to the post. LSP7, LSP8, ERC20, ERC721, ERC1155, etc.
218+
* **parentHash** If this post is a comment, the hash of the original post should go in here.
219+
* **childHash** If this post is a repost, the hash of the original post should go in here.
220+
221+
## Post Validator
222+
223+
This defines a validator smart contract where any Universal Profile can store proof that it knew some information at a given point in time.
224+
225+
### Motivation
226+
227+
One should not trust the author of a message to provide an accurate timestamp because it can be faked.
228+
Instead, a trustless timestamping service should be used to determine the message's creation date.
229+
This is possible using the blockchain as the source of time.
230+
231+
Furthermore, notice that timestamping a given hash is proof that the author
232+
was able to generate that hash at that time. This can be used to approach another problem:
233+
Cryptographic signatures are usually used to provide proof of ownership and timestamp.
234+
However, because a smart contract cannot sign, this method cannot be used for contract based accounts
235+
like an [ERC725Account](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-0-ERC725Account.md).
236+
Current practice if for an Externally Owned Address (EOA) to sign on behalf of the contract.
237+
However, it's hard to know if the EOA had permissions to sign at the time and to timestamp the signed message in a trustless way.
238+
239+
### Specification
240+
241+
This is a Solidity contract for a post validator that is tailored for Universal Profiles (UPs) and content publishing.
242+
The contract has two functions: `post` and `postWithJsonUrl`.
243+
244+
The `post` function allows a UP to make a post by emitting a `NewPost` event with the postHash and the UP's address as the indexed arguments.
245+
246+
The `postWithJsonUrl` function extends the `post` function by allowing the UP to also specify a reference to the latest Social Media Record in the `jsonUrl` argument.
247+
This function first verifies that the UP implements the ERC725Y standard (which includes a key/value store) and then sends a transaction to the UP to update the registry reference in the UP's key/value store with the `jsonUrl` value.
248+
249+
### Implementation
250+
251+
```solidity
252+
// SPDX-License-Identifier: MIT
253+
254+
pragma solidity ^0.8.7;
255+
256+
import { Context } from "@openzeppelin/contracts/utils/Context.sol";
257+
import {_INTERFACEID_ERC725Y} from "@erc725/smart-contracts/contracts/constants.sol";
258+
import { OwnableUnset } from "@erc725/smart-contracts/contracts/custom/OwnableUnset.sol";
259+
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
260+
import { ILSP6KeyManager} from "@lukso/lsp-smart-contracts/contracts/LSP6KeyManager/ILSP6KeyManager.sol";
261+
262+
/**
263+
* @title LSP19 post validator
264+
* @notice A validator tailored for Universal Profiles and content publishing
265+
* @dev Writes to the Universal Profile key/value store
266+
*/
267+
contract LSP19PostValidator is Context {
268+
269+
bytes32 public constant REGISTRY_KEY = keccak256("LSP19SocialRegistry");
270+
271+
event NewPost(bytes32 indexed postHash, address indexed author);
272+
273+
/**
274+
* @notice Universal Profile (message sender) makes a post
275+
* @param postHash will pushed in an event, with the _msgSender, in order to validate the author and the timestamp of the post
276+
*/
277+
function post(bytes32 postHash) public {
278+
// Save the timestamp as a blockchain event
279+
emit newPost(postHash, _msgSender());
280+
}
281+
282+
/**
283+
* @notice Universal Profile (message sender) makes a post
284+
* @dev This contract must have permissions to write on the Universal Profile
285+
* @param postHash will pushed in an event, with the _msgSender, in order to validate the author and the timestamp of the post
286+
* @param jsonUrl Reference to the latest Social Media Record of the sender
287+
*/
288+
function postWithJsonUrl(bytes32 postHash, bytes calldata jsonUrl) public {
289+
290+
// Save the timestamp as a blockchain event
291+
post(postHash);
292+
293+
// Verify sender supports the IERC725Y standard
294+
require(ERC165Checker.supportsERC165(_msgSender()), "Sender must implement ERC165. A UP does.");
295+
require(ERC165Checker.supportsInterface(_msgSender(), _INTERFACEID_ERC725Y), "Sender must implement IERC725Y (key/value store). A UP does");
296+
297+
// Create the tx to update the registry reference in the UP
298+
bytes memory encodedCall = abi.encodeWithSelector(
299+
bytes4(keccak256(bytes("setData(bytes32,bytes)"))), //function.selector
300+
REGISTRY_KEY, jsonUrl
301+
);
302+
303+
// Send the setData tx to the UP
304+
ILSP6KeyManager( OwnableUnset(_msgSender()).owner() ).execute(encodedCall);
305+
}
306+
}
307+
```
308+
309+
## Copyright
310+
311+
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).

0 commit comments

Comments
 (0)