405 lines
12 KiB
Markdown
405 lines
12 KiB
Markdown
|
|
# IPFS Storage Architecture for Motr Enclave
|
||
|
|
|
||
|
|
Decentralized storage layer for Nebula wallet's encrypted key enclave using IPFS via Helia with Circuit Relay connectivity.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ NEBULA WALLET STORAGE │
|
||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
|
||
|
|
│ │ Browser Environment │ │ Sonr Relay Infrastructure │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ ┌───────────────────┐ │ WSS │ ┌───────────────────────────┐ │ │
|
||
|
|
│ │ │ Motr Enclave │ │◄───────►│ │ ipfs-service-provider │ │ │
|
||
|
|
│ │ │ (WASM Plugin) │ │ │ │ + circuitRelayServer() │ │ │
|
||
|
|
│ │ └───────────────────┘ │ │ └───────────────────────────┘ │ │
|
||
|
|
│ │ │ │ │ │ │ │
|
||
|
|
│ │ ▼ │ │ ▼ │ │
|
||
|
|
│ │ ┌───────────────────┐ │ │ ┌───────────────────────────┐ │ │
|
||
|
|
│ │ │ Helia Client │ │ │ │ Helia Node │ │ │
|
||
|
|
│ │ │ (Browser SDK) │──┼─────────┼──│ (Content Pinning) │ │ │
|
||
|
|
│ │ └───────────────────┘ │ Relay │ └───────────────────────────┘ │ │
|
||
|
|
│ │ │ │ │ │ │ │
|
||
|
|
│ └───────────┼─────────────┘ └──────────────┼──────────────────┘ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ /p2p-circuit │ DHT / Bitswap │
|
||
|
|
│ ▼ ▼ │
|
||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ IPFS Network │ │
|
||
|
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Data Classification
|
||
|
|
|
||
|
|
### MUST Store on IPFS
|
||
|
|
|
||
|
|
| Data Type | Format | Mutability |
|
||
|
|
|-----------|--------|------------|
|
||
|
|
| Encrypted DB Blob | Raw bytes | Immutable (new CID per version) |
|
||
|
|
| DID Document | DAG-CBOR | Mutable via IPNS |
|
||
|
|
| UCAN Delegations | DAG-CBOR | Immutable CID references |
|
||
|
|
| Verification Methods | DAG-CBOR | Part of DID Document |
|
||
|
|
|
||
|
|
### MUST NOT Store on IPFS
|
||
|
|
|
||
|
|
| Data Type | Reason |
|
||
|
|
|-----------|--------|
|
||
|
|
| MPC Key Shares | Core secret material |
|
||
|
|
| WebAuthn PRF Output | Decryption key derivation |
|
||
|
|
| Session Tokens | Ephemeral, local-only |
|
||
|
|
| Plaintext Credentials | Security violation |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Browser Client Requirements
|
||
|
|
|
||
|
|
### Transport Configuration
|
||
|
|
|
||
|
|
The browser Helia client MUST configure these transports:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
transports: [
|
||
|
|
webSockets({ filter: filters.all }),
|
||
|
|
webRTC(),
|
||
|
|
circuitRelayTransport({ discoverRelays: 2 })
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Listen Addresses
|
||
|
|
|
||
|
|
The client MUST listen on:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
addresses: {
|
||
|
|
listen: ['/p2p-circuit', '/webrtc']
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Required Services
|
||
|
|
|
||
|
|
The `identify` service is MANDATORY for Circuit Relay v2:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
services: {
|
||
|
|
identify: identify()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Bootstrap Peers
|
||
|
|
|
||
|
|
The client MUST bootstrap with at least 2 Sonr relay multiaddrs:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
peerDiscovery: [
|
||
|
|
bootstrap({
|
||
|
|
list: [
|
||
|
|
'/dns4/relay1.sonr.io/tcp/443/wss/p2p/<PEER_ID>',
|
||
|
|
'/dns4/relay2.sonr.io/tcp/443/wss/p2p/<PEER_ID>'
|
||
|
|
]
|
||
|
|
})
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Complete Client Implementation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createHelia } from 'helia'
|
||
|
|
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||
|
|
import { webRTC } from '@libp2p/webrtc'
|
||
|
|
import { webSockets } from '@libp2p/websockets'
|
||
|
|
import { bootstrap } from '@libp2p/bootstrap'
|
||
|
|
import { identify } from '@libp2p/identify'
|
||
|
|
import { noise } from '@chainsafe/libp2p-noise'
|
||
|
|
import { yamux } from '@chainsafe/libp2p-yamux'
|
||
|
|
import * as filters from '@libp2p/websockets/filters'
|
||
|
|
|
||
|
|
const SONR_RELAYS = [
|
||
|
|
'/dns4/relay1.sonr.io/tcp/443/wss/p2p/<PEER_ID>',
|
||
|
|
'/dns4/relay2.sonr.io/tcp/443/wss/p2p/<PEER_ID>'
|
||
|
|
]
|
||
|
|
|
||
|
|
export async function createStorageClient(): Promise<Helia> {
|
||
|
|
return await createHelia({
|
||
|
|
libp2p: {
|
||
|
|
addresses: {
|
||
|
|
listen: ['/p2p-circuit', '/webrtc']
|
||
|
|
},
|
||
|
|
transports: [
|
||
|
|
webSockets({ filter: filters.all }),
|
||
|
|
webRTC(),
|
||
|
|
circuitRelayTransport({ discoverRelays: 2 })
|
||
|
|
],
|
||
|
|
connectionEncrypters: [noise()],
|
||
|
|
streamMuxers: [yamux()],
|
||
|
|
peerDiscovery: [
|
||
|
|
bootstrap({ list: SONR_RELAYS })
|
||
|
|
],
|
||
|
|
services: {
|
||
|
|
identify: identify()
|
||
|
|
},
|
||
|
|
connectionGater: {
|
||
|
|
denyDialMultiaddr: () => false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Relay Server Requirements
|
||
|
|
|
||
|
|
### Circuit Relay Configuration
|
||
|
|
|
||
|
|
The relay server MUST enable `circuitRelayServer` with these constraints:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
|
||
|
|
|
||
|
|
services: {
|
||
|
|
relay: circuitRelayServer({
|
||
|
|
hopTimeout: 30 * 1000,
|
||
|
|
reservations: {
|
||
|
|
maxReservations: 100,
|
||
|
|
reservationClearInterval: 300 * 1000,
|
||
|
|
applyDefaultLimit: true,
|
||
|
|
defaultDurationLimit: 2 * 60 * 1000,
|
||
|
|
defaultDataLimit: BigInt(1 << 20)
|
||
|
|
},
|
||
|
|
maxInboundHopStreams: 64,
|
||
|
|
maxOutboundHopStreams: 128
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Listen Addresses
|
||
|
|
|
||
|
|
The relay MUST listen on:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
addresses: {
|
||
|
|
listen: [
|
||
|
|
'/ip4/0.0.0.0/tcp/4001',
|
||
|
|
'/ip4/0.0.0.0/tcp/4003/ws',
|
||
|
|
'/ip4/0.0.0.0/udp/4001/quic-v1',
|
||
|
|
'/webrtc'
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Environment Variables
|
||
|
|
|
||
|
|
| Variable | Required | Description |
|
||
|
|
|----------|----------|-------------|
|
||
|
|
| `ENABLE_CIRCUIT_RELAY` | Yes | Set to `1` |
|
||
|
|
| `CR_DOMAIN` | Yes | Relay domain (e.g., `relay.sonr.org`) |
|
||
|
|
| `IPFS_TCP_PORT` | Yes | TCP port (default: `4001`) |
|
||
|
|
| `IPFS_WS_PORT` | Yes | WebSocket port (default: `4003`) |
|
||
|
|
| `COORD_NAME` | No | Node identifier for coordination |
|
||
|
|
|
||
|
|
### Infrastructure Requirements
|
||
|
|
|
||
|
|
| Requirement | Specification |
|
||
|
|
|-------------|---------------|
|
||
|
|
| SSL/TLS | Valid certificate for WSS |
|
||
|
|
| Reverse Proxy | Nginx forwarding 443 → `IPFS_WS_PORT` |
|
||
|
|
| Redundancy | Minimum 2 relay nodes |
|
||
|
|
| Uptime | 99.9% availability target |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Storage Operations
|
||
|
|
|
||
|
|
### Enclave Persistence
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { unixfs } from '@helia/unixfs'
|
||
|
|
import type { CID } from 'multiformats/cid'
|
||
|
|
import type { Helia } from 'helia'
|
||
|
|
|
||
|
|
export class EnclaveStorage {
|
||
|
|
private fs: UnixFS
|
||
|
|
|
||
|
|
constructor(private helia: Helia) {
|
||
|
|
this.fs = unixfs(helia)
|
||
|
|
}
|
||
|
|
|
||
|
|
async persist(encryptedBlob: Uint8Array): Promise<CID> {
|
||
|
|
const cid = await this.fs.addBytes(encryptedBlob)
|
||
|
|
await this.helia.pins.add(cid)
|
||
|
|
return cid
|
||
|
|
}
|
||
|
|
|
||
|
|
async retrieve(cid: CID): Promise<Uint8Array> {
|
||
|
|
const chunks: Uint8Array[] = []
|
||
|
|
for await (const chunk of this.fs.cat(cid)) {
|
||
|
|
chunks.push(chunk)
|
||
|
|
}
|
||
|
|
return concatUint8Arrays(chunks)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### DID Publishing
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { ipns } from '@helia/ipns'
|
||
|
|
import { dagCbor } from '@helia/dag-cbor'
|
||
|
|
import type { PrivateKey } from '@libp2p/interface'
|
||
|
|
|
||
|
|
export class DIDPublisher {
|
||
|
|
private name: IPNS
|
||
|
|
private cbor: DagCBOR
|
||
|
|
|
||
|
|
constructor(private helia: Helia) {
|
||
|
|
this.name = ipns(helia)
|
||
|
|
this.cbor = dagCbor(helia)
|
||
|
|
}
|
||
|
|
|
||
|
|
async publish(didDocument: object, key: PrivateKey): Promise<string> {
|
||
|
|
const cid = await this.cbor.add(didDocument)
|
||
|
|
await this.name.publish(key, cid)
|
||
|
|
return `/ipns/${key.publicKey.toCID()}`
|
||
|
|
}
|
||
|
|
|
||
|
|
async resolve(ipnsName: string): Promise<object> {
|
||
|
|
const result = await this.name.resolve(ipnsName)
|
||
|
|
return await this.cbor.get(result.cid)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Security Requirements
|
||
|
|
|
||
|
|
### Encryption
|
||
|
|
|
||
|
|
| Layer | Requirement |
|
||
|
|
|-------|-------------|
|
||
|
|
| Transport | TLS 1.3 (WSS) + libp2p Noise protocol |
|
||
|
|
| Storage | AES-256-GCM with WebAuthn PRF-derived key |
|
||
|
|
| Integrity | SHA-256 content hash (CID) |
|
||
|
|
|
||
|
|
### Relay Trust Model
|
||
|
|
|
||
|
|
| Property | Guarantee |
|
||
|
|
|----------|-----------|
|
||
|
|
| Zero-Knowledge | Relay MUST NOT see plaintext data |
|
||
|
|
| No Persistence | Relay MUST NOT store relayed content |
|
||
|
|
| Rate Limiting | Relay MUST enforce per-peer limits |
|
||
|
|
|
||
|
|
### Client Responsibilities
|
||
|
|
|
||
|
|
| Responsibility | Requirement |
|
||
|
|
|----------------|-------------|
|
||
|
|
| Encrypt before upload | All data MUST be encrypted client-side |
|
||
|
|
| Verify on download | Client MUST verify CID matches content |
|
||
|
|
| Local CID storage | Latest CID MUST persist in IndexedDB |
|
||
|
|
| Key isolation | Decryption keys MUST NOT leave browser |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## API Specification
|
||
|
|
|
||
|
|
### StorageClient Interface
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface StorageClient {
|
||
|
|
connect(): Promise<void>
|
||
|
|
disconnect(): Promise<void>
|
||
|
|
isConnected(): boolean
|
||
|
|
|
||
|
|
persistEnclave(encrypted: Uint8Array): Promise<CID>
|
||
|
|
retrieveEnclave(cid: CID): Promise<Uint8Array>
|
||
|
|
|
||
|
|
publishDID(doc: DIDDocument, key: PrivateKey): Promise<string>
|
||
|
|
resolveDID(name: string): Promise<DIDDocument>
|
||
|
|
|
||
|
|
getStatus(): StorageStatus
|
||
|
|
}
|
||
|
|
|
||
|
|
interface StorageStatus {
|
||
|
|
peerId: string
|
||
|
|
connected: boolean
|
||
|
|
peerCount: number
|
||
|
|
relayCount: number
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Relay API Endpoints
|
||
|
|
|
||
|
|
| Endpoint | Method | Response |
|
||
|
|
|----------|--------|----------|
|
||
|
|
| `/ipfs` | GET | `{ ipfsId, peers, relays }` |
|
||
|
|
| `/ipfs/node` | GET | Full node configuration |
|
||
|
|
| `/ipfs/relays` | POST | Known relay list |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Dependencies
|
||
|
|
|
||
|
|
### Browser SDK
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"helia": "^5.0.0",
|
||
|
|
"@helia/unixfs": "^4.0.0",
|
||
|
|
"@helia/ipns": "^8.0.0",
|
||
|
|
"@helia/dag-cbor": "^4.0.0",
|
||
|
|
"@libp2p/circuit-relay-v2": "^3.0.0",
|
||
|
|
"@libp2p/webrtc": "^5.0.0",
|
||
|
|
"@libp2p/websockets": "^9.0.0",
|
||
|
|
"@libp2p/bootstrap": "^11.0.0",
|
||
|
|
"@libp2p/identify": "^3.0.0",
|
||
|
|
"@chainsafe/libp2p-noise": "^16.0.0",
|
||
|
|
"@chainsafe/libp2p-yamux": "^7.0.0",
|
||
|
|
"multiformats": "^13.0.0"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Relay Server
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"helia": "^5.0.0",
|
||
|
|
"helia-coord": "^3.0.0",
|
||
|
|
"@libp2p/circuit-relay-v2": "^3.0.0",
|
||
|
|
"@libp2p/tcp": "^10.0.0",
|
||
|
|
"@libp2p/websockets": "^9.0.0",
|
||
|
|
"@libp2p/webrtc": "^5.0.0"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
src/
|
||
|
|
├── storage/
|
||
|
|
│ ├── client.ts # Helia client factory
|
||
|
|
│ ├── enclave.ts # Enclave persistence
|
||
|
|
│ ├── did.ts # DID/IPNS operations
|
||
|
|
│ └── types.ts # TypeScript interfaces
|
||
|
|
deploy/
|
||
|
|
├── relay/
|
||
|
|
│ ├── docker-compose.yml
|
||
|
|
│ └── config.env
|
||
|
|
└── nginx/
|
||
|
|
└── relay.conf # WSS proxy config
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## References
|
||
|
|
|
||
|
|
- [Helia](https://github.com/ipfs/helia)
|
||
|
|
- [libp2p Circuit Relay v2 Spec](https://github.com/libp2p/specs/blob/master/relay/circuit-v2.md)
|
||
|
|
- [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider)
|
||
|
|
- [UCAN Spec v1.0.0-rc.1](https://github.com/ucan-wg/spec)
|