Skip to content

Commit 459ce4f

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

File tree

1 file changed

+335
-0
lines changed

1 file changed

+335
-0
lines changed

LSPs/LSP-29-EncryptedAssets.md

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

0 commit comments

Comments
 (0)