Skip to content

Commit 46d43ba

Browse files
committed
feat: create LSP-29-EncryptedAssets.md
1 parent 6e8f175 commit 46d43ba

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed

LSPs/LSP-29-EncryptedAssets.md

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
---
2+
lip: 29
3+
title: Encrypted Assets
4+
author: b00ste
5+
discussions-to:
6+
status: Draft
7+
type: LSP
8+
created: 2025-01-08
9+
requires: ERC725Y, LSP2
10+
---
11+
12+
## Simple Summary
13+
14+
A standard for storing encrypted digital assets in [ERC725Y] smart contracts, enabling creators to manage token-gated content directly on their Universal Profile.
15+
16+
## Abstract
17+
18+
This standard defines a set of [ERC725Y] data keys for storing references to encrypted digital assets. The encrypted content is stored on IPFS, while metadata and access control information are encoded as [VerifiableURI] values in the Universal Profile's storage. The standard supports versioning, allowing creators to update content while preserving full revision history. Each asset can include social feed images for content preview and discovery.
19+
20+
## Motivation
21+
22+
LUKSO currently has no standard for storing encrypted, token-gated digital assets. While LSP4 defines metadata for digital assets (LSP7/LSP8 tokens), it only supports unencrypted content. Creators who want to offer exclusive, encrypted content to token holders have no standardized way to:
23+
24+
1. **Store Encrypted Content**: No defined schema for encrypted asset metadata
25+
2. **Link to Creator**: No way to associate encrypted content with a Universal Profile
26+
3. **Track Versions**: No mechanism for updating content while preserving history
27+
4. **Enable Discovery**: No standard for enumerating a creator's encrypted offerings
28+
29+
LSP29 introduces a complete solution by:
30+
31+
1. **Defining a Schema**: Standardized JSON format for encrypted asset metadata
32+
2. **Centralizing on Profile**: All encrypted assets stored on the creator's Universal Profile
33+
3. **Supporting Versioning**: Append-only array with revision tracking preserves full history
34+
4. **Enabling Discovery**: Easy enumeration via array iteration and mapping lookups
35+
5. **Flexible Access Control**: Each asset can reference different token gates for decryption
36+
6. **Social Feed Integration**: Images for content preview and social discovery
37+
38+
## Specification
39+
40+
### ERC725Y Data Keys
41+
42+
#### LSP29EncryptedAssets[]
43+
44+
An [LSP2 Array] of [VerifiableURI] values, each pointing to an encrypted asset's JSON metadata on IPFS.
45+
46+
```json
47+
{
48+
"name": "LSP29EncryptedAssets[]",
49+
"key": "0x1965f98377ddff08e78c93d820cc8de4eeb331e684b7724bce0debb1958386c3",
50+
"keyType": "Array",
51+
"valueType": "bytes",
52+
"valueContent": "VerifiableURI"
53+
}
54+
```
55+
56+
For more information about how to access each index of the `LSP29EncryptedAssets[]` array, see [LSP2 Array].
57+
58+
#### LSP29EncryptedAssetsMap
59+
60+
An [LSP2 Mapping] from a content identifier hash to the array index. This mapping supports two usage patterns:
61+
62+
1. **Latest version**: Hash of content ID only → points to the most recent revision
63+
2. **Specific version**: Hash of content ID + revision → points to that exact revision
64+
65+
```json
66+
{
67+
"name": "LSP29EncryptedAssetsMap:<bytes20>",
68+
"key": "0x2b9a7a38a67cedc507c20000<bytes20>",
69+
"keyType": "Mapping",
70+
"valueType": "uint128",
71+
"valueContent": "Number"
72+
}
73+
```
74+
75+
**For latest version**: `<bytes20>` is the first 20 bytes of `keccak256(contentId)`, where `contentId` is the string identifier chosen by the creator. This entry is updated each time a new revision is added.
76+
77+
**For specific version**: `<bytes20>` is the first 20 bytes of `keccak256(abi.encodePacked(contentId, uint32(revision)))`. This entry is immutable once set.
78+
79+
**Examples** for content ID `"premium-content"`:
80+
81+
| Lookup Type | Hash Input | Key |
82+
| ----------- | ----------------------------------------------------------- | ----------------------------------- |
83+
| Latest | `keccak256("premium-content")` | `0x2b9a7a38a67cedc507c200008a5b...` |
84+
| Revision 1 | `keccak256(abi.encodePacked("premium-content", uint32(1)))` | `0x2b9a7a38a67cedc507c2000012ab...` |
85+
| Revision 2 | `keccak256(abi.encodePacked("premium-content", uint32(2)))` | `0x2b9a7a38a67cedc507c200003c7f...` |
86+
87+
#### LSP29EncryptedAssetRevisionCount
88+
89+
An [LSP2 Mapping] from a content identifier hash to the total number of revisions for that content.
90+
91+
```json
92+
{
93+
"name": "LSP29EncryptedAssetRevisionCount:<bytes32>",
94+
"key": "0xb41f63e335c22bded8140000<bytes20>",
95+
"keyType": "Mapping",
96+
"valueType": "uint128",
97+
"valueContent": "Number"
98+
}
99+
```
100+
101+
Where `<bytes20>` is the first 20 bytes of `keccak256(contentId)`.
102+
103+
### JSON Schema
104+
105+
The [VerifiableURI] stored in the array MUST point to a JSON file on IPFS conforming to the following schema:
106+
107+
```json
108+
{
109+
"LSP29EncryptedAsset": {
110+
"version": "1.0.0",
111+
"id": "<string>",
112+
"title": "<string>",
113+
"description": "<string>",
114+
"images": "[[<image>, ...], ...]",
115+
"revision": "<number>",
116+
"createdAt": "<string>",
117+
"file": {
118+
"type": "<string>",
119+
"name": "<string>",
120+
"size": "<number>",
121+
"lastModified": "<number>",
122+
"hash": "<string>"
123+
},
124+
"encryption": {
125+
"method": "<string>",
126+
"ciphertext": "<string>",
127+
"dataToEncryptHash": "<string>",
128+
"accessControlConditions": "<array>",
129+
"decryptionCode": "<string>",
130+
"decryptionParams": "<object>"
131+
},
132+
"chunks": {
133+
"cids": "[<string>, ...]",
134+
"iv": "<string>",
135+
"totalSize": "<number>"
136+
}
137+
}
138+
}
139+
```
140+
141+
#### LSP29EncryptedAsset
142+
143+
| Key | Type | Required | Description |
144+
| ------------- | ------ | -------- | --------------------------------------------------------- |
145+
| `version` | string | Yes | Schema version (e.g., `"1.0.0"`) |
146+
| `id` | string | Yes | Unique content identifier chosen by creator |
147+
| `title` | string | Yes | Human-readable title for the content |
148+
| `description` | string | No | Human-readable description of the content |
149+
| `images` | array | No | Social feed images for content preview |
150+
| `revision` | number | Yes | Version number starting at 1, incremented for each update |
151+
| `createdAt` | string | Yes | ISO 8601 timestamp when this revision was created |
152+
| `file` | object | Yes | Technical metadata about the encrypted file |
153+
| `encryption` | object | Yes | Encryption metadata for decryption |
154+
| `chunks` | object | Yes | Chunked storage information |
155+
156+
#### file
157+
158+
| Key | Type | Required | Description |
159+
| -------------- | ------ | -------- | ---------------------------------------------------- |
160+
| `type` | string | Yes | MIME type of the original file (e.g., `"video/mp4"`) |
161+
| `name` | string | Yes | Original filename |
162+
| `size` | number | Yes | Original file size in bytes (before encryption) |
163+
| `lastModified` | number | No | Unix timestamp (ms) of file's last modification |
164+
| `hash` | string | Yes | Hash of the original file content (SHA-256, hex) |
165+
166+
#### images
167+
168+
An array of image arrays for social feed functionality. Each inner array contains different sizes of the same image, following the LSP4Metadata images structure. The first image in the first inner array should be used as the main preview image.
169+
170+
**Structure:**
171+
172+
```json
173+
"images": [
174+
[
175+
{
176+
"width": "number",
177+
"height": "number",
178+
"url": "string",
179+
"verification": {
180+
"method": "keccak256(bytes)",
181+
"data": "string"
182+
}
183+
},
184+
{
185+
"width": "number",
186+
"height": "number",
187+
"url": "string",
188+
"verification": {
189+
"method": "ecdsa",
190+
"data": "string",
191+
"source": "string"
192+
}
193+
}
194+
],
195+
[
196+
// Additional image sets
197+
]
198+
]
199+
```
200+
201+
| Key | Type | Required | Description |
202+
| -------------- | ------ | -------- | --------------------------------------------------- |
203+
| `width` | number | Yes | Image width in pixels |
204+
| `height` | number | Yes | Image height in pixels |
205+
| `url` | string | Yes | URL to the image file |
206+
| `verification` | object | Yes | Verification data for image authenticity |
207+
| `method` | string | Yes | Verification method (`keccak256(bytes)` or `ecdsa`) |
208+
| `data` | string | Yes | Hash or signature for verification |
209+
| `source` | string | No | URL for signature verification (ecdsa method only) |
210+
211+
#### encryption
212+
213+
| Key | Type | Required | Description |
214+
| ------------------------- | ------ | -------- | ---------------------------------------------------------- |
215+
| `method` | string | Yes | Encryption method identifier (see supported methods below) |
216+
| `ciphertext` | string | Yes | Encrypted symmetric key |
217+
| `dataToEncryptHash` | string | Yes | Hash of the encrypted data for verification |
218+
| `accessControlConditions` | array | Yes | Conditions for decryption access |
219+
| `decryptionCode` | string | Yes | Code or reference for decryption logic |
220+
| `decryptionParams` | object | Yes | Dynamic parameters embedded in `decryptionCode` |
221+
222+
The `decryptionParams` object contains the dynamic values that are hardcoded into the `decryptionCode`. This enables UI display and content filtering without parsing the decryption code. See [Decryption Parameters Security](#decryption-parameters-security) for important security considerations.
223+
224+
**Supported Encryption Methods:**
225+
226+
| Method | Description | Example `decryptionParams` |
227+
| ------------------------------ | -------------------------------------- | -------------------------------------------------------------------------------------- |
228+
| `lit-digital-asset-balance-v1` | Digital asset balance via Lit Protocol | `{ "tokenAddress": "0x...", "requiredBalance": "1000000" }` |
229+
| `lit-lsp8-ownership-v1` | LSP8 NFT ownership via Lit Protocol | `{ "tokenAddress": "0x...", "requiredTokenId": "42" }` |
230+
| `lit-lsp26-follower-v1` | LSP26 on-chain follower check | `{ "followedAddresses": ["0x...", "0x..."] }` |
231+
| `lit-social-followers-v1` | Off-chain social verification | `{ "platform": "twitter", "creatorHandle": "@creator", "requiredFollowers": "10000" }` |
232+
| `lit-time-locked-v1` | Time-lock via Lit Protocol | `{ "unlockTimestamp": "1735689600" }` |
233+
234+
#### chunks
235+
236+
| Key | Type | Required | Description |
237+
| ----------- | ------ | -------- | ------------------------------------------------------- |
238+
| `cids` | array | Yes | Array of IPFS CIDs for encrypted content chunks |
239+
| `iv` | string | Yes | Initialization vector for symmetric encryption (base64) |
240+
| `totalSize` | number | Yes | Total size of encrypted content in bytes |
241+
242+
### Data Flow
243+
244+
#### Creating New Content
245+
246+
1. Creator chooses a unique `id` (e.g., `"exclusive-album-2025"`)
247+
2. Set `revision` to `1`
248+
3. Set `createdAt` to current ISO 8601 timestamp
249+
4. Encrypt content and upload chunks to IPFS
250+
5. Create JSON metadata and upload to IPFS
251+
6. Encode as [VerifiableURI]
252+
7. Write to ERC725Y storage:
253+
- Append to `LSP29EncryptedAssets[]` array
254+
- Set `LSP29EncryptedAssetsMap:<keccak256(id)>` to new array index (latest)
255+
- Set `LSP29EncryptedAssetsMap:<keccak256(id + revision)>` to new array index (version 1)
256+
- Set `LSP29EncryptedAssetRevisionCount:<id>` to `1`
257+
258+
#### Updating Content (New Revision)
259+
260+
1. Use the same `id` as the original content
261+
2. Read `LSP29EncryptedAssetRevisionCount:<id>` to get current revision count
262+
3. Set `revision` to `currentCount + 1`
263+
4. Set `createdAt` to current ISO 8601 timestamp
264+
5. Encrypt new content and upload chunks to IPFS
265+
6. Create JSON metadata and upload to IPFS
266+
7. Encode as [VerifiableURI]
267+
8. Write to ERC725Y storage:
268+
- Append to `LSP29EncryptedAssets[]` array
269+
- Update `LSP29EncryptedAssetsMap:<keccak256(id)>` to new array index (latest)
270+
- Set `LSP29EncryptedAssetsMap:<keccak256(id + revision)>` to new array index (this version)
271+
- Increment `LSP29EncryptedAssetRevisionCount:<id>`
272+
273+
#### Reading Latest Version
274+
275+
1. Compute key: `LSP29EncryptedAssetsMap:<keccak256(id)>`
276+
2. Read array index from mapping
277+
3. Read [VerifiableURI] from `LSP29EncryptedAssets[index]`
278+
4. Fetch and verify JSON from IPFS
279+
280+
#### Reading Specific Version
281+
282+
1. Compute key: `LSP29EncryptedAssetsMap:<keccak256(abi.encodePacked(id, uint32(revision)))>`
283+
2. Read array index from mapping
284+
3. Read [VerifiableURI] from `LSP29EncryptedAssets[index]`
285+
4. Fetch and verify JSON from IPFS
286+
287+
#### Enumerating All Versions
288+
289+
1. Read `LSP29EncryptedAssetRevisionCount:<id>` to get total count
290+
2. For each revision 1 to count:
291+
- Compute key: `LSP29EncryptedAssetsMap:<keccak256(abi.encodePacked(id, uint32(revision)))>`
292+
- Read array index and fetch corresponding element
293+
294+
## Rationale
295+
296+
### Append-Only Array
297+
298+
The array is designed to be append-only to ensure:
299+
300+
- **Immutability**: Once content is published, it cannot be removed or altered
301+
- **Audit Trail**: Full history of all content versions is preserved
302+
- **Verifiability**: Third parties can verify the complete history
303+
304+
### Single Mapping with Dual Purpose
305+
306+
A single mapping (`LSP29EncryptedAssetsMap`) serves both use cases through different hash inputs:
307+
308+
- **Latest version**: Hash of content ID only → always points to most recent revision (updated on each new version)
309+
- **Specific version**: Hash of content ID + revision → immutable pointer to that exact revision
310+
311+
This design reduces the number of data keys while maintaining O(1) lookup for both patterns.
312+
313+
### Content ID Design
314+
315+
Content IDs are creator-chosen strings rather than hashes because:
316+
317+
- **Human Readable**: IDs like `"premium-album-2025"` are meaningful
318+
- **Stable**: Same ID persists across revisions
319+
- **Flexible**: Creators control their namespace
320+
321+
### Revision Count
322+
323+
A separate revision count mapping enables:
324+
325+
- **Enumeration**: List all versions without iterating entire array
326+
- **Validation**: Verify expected revision number before write
327+
- **Efficiency**: O(1) lookup of version count
328+
329+
## Security Considerations
330+
331+
### Content Immutability
332+
333+
While the array is append-only at the application level, ERC725Y storage can technically be overwritten by the Universal Profile owner. Consumers should:
334+
335+
- Verify content hashes match [VerifiableURI] declarations
336+
- Consider timestamps when multiple versions exist
337+
- Be aware that "latest" mapping can be updated
338+
339+
### Decryption Parameters Security
340+
341+
The `decryptionParams` field exists for UI/querying purposes and MUST match the hardcoded values in `decryptionCode`. Applications SHOULD:
342+
343+
- Verify `decryptionParams` values match those embedded in `decryptionCode` when possible
344+
- Display warnings to users if discrepancies are detected
345+
- Never rely solely on `decryptionParams` for access control enforcement
346+
- Treat `decryptionCode` as the authoritative source of truth
347+
348+
Actual access control MUST be enforced by the decryption mechanism (e.g., Lit Protocol access control conditions embedded in `decryptionCode`). Applications MUST NOT rely solely on the JSON `decryptionParams` field for security.
349+
350+
### Method Versioning
351+
352+
The `method` field includes a version suffix (e.g., `lit-lsp7-balance-v1`). When creating new encryption methods:
353+
354+
- Use unique, descriptive method identifiers
355+
- Include version suffix for future compatibility (e.g., `-v1`, `-v2`)
356+
- Document required `decryptionParams` schema for each method
357+
- Maintain backward compatibility when incrementing versions
358+
359+
### Content ID Collisions
360+
361+
Content IDs are user-chosen strings. Applications SHOULD:
362+
363+
- Validate uniqueness before creating new content with an ID that may already exist
364+
- Check `LSP29EncryptedAssetRevisionCount:<id>` to detect existing content
365+
- Handle collisions gracefully (e.g., append suffix or reject)
366+
367+
### IPFS Persistence
368+
369+
Content stored on IPFS requires pinning for persistence. Applications SHOULD:
370+
371+
- Use pinning services for long-term storage
372+
- Provide mechanisms for creators to ensure content availability
373+
- Handle cases where IPFS content may become unavailable
374+
375+
## Copyright
376+
377+
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
378+
379+
[ERC725Y]: https://github.com/ethereum/ercs/blob/master/ERCS/erc-725.md#erc725y
380+
[LSP2 Array]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#array
381+
[LSP2 Mapping]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping
382+
[VerifiableURI]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#verifiableuri

0 commit comments

Comments
 (0)