Files
next.orly.dev/docs/NIP-NRC.md
woikos 205f23fc0c
Some checks failed
Go / build-and-release (push) Has been cancelled
Add message segmentation to NRC protocol spec (v0.48.15)
- Add CHUNK response type for large payload handling
- Document chunking threshold (40KB) accounting for encryption overhead
- Specify chunk message format with messageId, index, total, data fields
- Add sender chunking process with Base64 encoding steps
- Add receiver reassembly process with buffer management
- Document 60-second timeout for incomplete chunk buffers
- Update client/bridge implementation notes with chunking requirements
- Add Smesh as reference implementation for client-side chunking

Files modified:
- docs/NIP-NRC.md: Added Message Segmentation section and updated impl notes
- pkg/version/version: v0.48.14 -> v0.48.15

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 11:29:31 +01:00

317 lines
11 KiB
Markdown

# NIP-XX: Nostr Relay Connect (NRC)
`draft` `optional`
## Abstract
This NIP defines a protocol for exposing a private Nostr relay through a public relay, enabling access to relays behind NAT, firewalls, or on devices without public IP addresses. It uses end-to-end encrypted events to tunnel standard Nostr protocol messages through a rendezvous relay.
## Motivation
Users want to run personal relays for:
- Private data synchronization across devices
- Full control over event storage
- Offline-first applications with sync capability
However, personal relays often run:
- Behind NAT without public IP addresses
- On mobile devices
- On home servers without port forwarding capability
NRC solves this by tunneling Nostr protocol messages through encrypted events on a public relay, similar to how [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) tunnels wallet operations.
## Specification
### Event Kinds
| Kind | Name | Description |
|-------|--------------|------------------------------------------|
| 24891 | NRC Request | Ephemeral, client→relay wrapped message |
| 24892 | NRC Response | Ephemeral, relay→client wrapped message |
### Connection URI
The connection URI format is:
```
nostr+relayconnect://<relay-pubkey>?relay=<rendezvous-relay>&secret=<client-secret>[&name=<device-name>]
```
Parameters:
- `relay-pubkey`: The public key of the private relay (64-char hex)
- `relay`: The WebSocket URL of the rendezvous relay (URL-encoded)
- `secret`: A 32-byte hex-encoded secret used to derive the conversation key
- `name` (optional): Human-readable device identifier for management
Example:
```
nostr+relayconnect://a1b2c3d4e5f6...?relay=wss%3A%2F%2Frelay.example.com&secret=0123456789abcdef...&name=phone
```
### Alternative: CAT Token Authentication
For privacy-preserving access, NRC supports Cashu Access Tokens (CAT) instead of static secrets:
```
nostr+relayconnect://<relay-pubkey>?relay=<rendezvous-relay>&auth=cat&mint=<mint-url>
```
When using CAT authentication:
1. Client obtains a CAT token from the mint with scope `nrc`
2. Client includes the token in request events using a `cashu` tag
3. Bridge verifies the token and re-authorizes via ACL on each request
### Message Flow
```
┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────────┐
│ Client │────▶│ Public Relay│────▶│ Bridge │────▶│Private Relay│
│ │◀────│ (rendezvous)│◀────│ │◀────│ │
└─────────┘ └─────────────┘ └─────────┘ └─────────────┘
│ │
└────────── NIP-44 encrypted ────────┘
```
1. **Client** wraps Nostr messages in kind 24891 events, encrypts content with NIP-44
2. **Public relay** forwards events based on `p` tags (cannot decrypt content)
3. **Bridge** (running alongside private relay) decrypts and forwards to local relay
4. **Private relay** processes the message normally
5. **Bridge** wraps response in kind 24892, encrypts, and publishes
6. **Client** receives kind 24892 events and decrypts the response
### Request Event (Kind 24891)
```json
{
"kind": 24891,
"content": "<nip44_encrypted_json>",
"tags": [
["p", "<relay_pubkey>"],
["encryption", "nip44_v2"],
["session", "<session_id>"]
],
"pubkey": "<client_pubkey>",
"created_at": <unix_timestamp>,
"sig": "<signature>"
}
```
With CAT authentication, add:
```json
["cashu", "cashuA..."]
```
The encrypted content structure:
```json
{
"type": "EVENT" | "REQ" | "CLOSE" | "AUTH" | "COUNT",
"payload": <standard_nostr_message_array>
}
```
Where `payload` is the standard Nostr message array, e.g.:
- `["EVENT", <event_object>]`
- `["REQ", "<sub_id>", <filter1>, <filter2>, ...]`
- `["CLOSE", "<sub_id>"]`
- `["AUTH", <auth_event>]`
- `["COUNT", "<sub_id>", <filter1>, ...]`
### Response Event (Kind 24892)
```json
{
"kind": 24892,
"content": "<nip44_encrypted_json>",
"tags": [
["p", "<client_pubkey>"],
["encryption", "nip44_v2"],
["session", "<session_id>"],
["e", "<request_event_id>"]
],
"pubkey": "<relay_pubkey>",
"created_at": <unix_timestamp>,
"sig": "<signature>"
}
```
The encrypted content structure:
```json
{
"type": "EVENT" | "OK" | "EOSE" | "NOTICE" | "CLOSED" | "COUNT" | "AUTH" | "CHUNK",
"payload": <standard_nostr_response_array>
}
```
Where `payload` is the standard Nostr response array, e.g.:
- `["EVENT", "<sub_id>", <event_object>]`
- `["OK", "<event_id>", <success_bool>, "<message>"]`
- `["EOSE", "<sub_id>"]`
- `["NOTICE", "<message>"]`
- `["CLOSED", "<sub_id>", "<message>"]`
- `["COUNT", "<sub_id>", {"count": <n>}]`
- `["AUTH", "<challenge>"]`
- `[<chunk_object>]` (for CHUNK type, see Message Segmentation)
### Session Management
The `session` tag groups related request/response events, enabling:
- Multiple concurrent subscriptions through a single tunnel
- Correlation of responses to requests
- Session state tracking on the bridge
Session IDs SHOULD be randomly generated UUIDs or 32-byte hex strings.
### Encryption
All content is encrypted using [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) v2.
The conversation key is derived from:
- **Secret-based auth**: ECDH between client's secret key (derived from URI secret) and relay's public key
- **CAT auth**: ECDH between client's Nostr key and relay's public key
### Message Segmentation
Some Nostr events exceed the typical relay message size limits (commonly 64KB). NRC supports message segmentation to handle large payloads by splitting them into multiple chunks.
#### When to Chunk
Senders SHOULD chunk messages when the JSON-serialized response exceeds 40KB. This threshold accounts for:
- NIP-44 encryption overhead (~100 bytes)
- Base64 encoding expansion (~33%)
- Event wrapper overhead (tags, signature, etc.)
#### Chunk Message Format
When a response is too large, it is split into multiple CHUNK responses:
```json
{
"type": "CHUNK",
"payload": [{
"type": "CHUNK",
"messageId": "<uuid>",
"index": 0,
"total": 3,
"data": "<base64_encoded_chunk>"
}]
}
```
Fields:
- `messageId`: A unique identifier (UUID) for the chunked message, used to correlate chunks
- `index`: Zero-based chunk index (0, 1, 2, ...)
- `total`: Total number of chunks in this message
- `data`: Base64-encoded segment of the original message
#### Chunking Process (Sender)
1. Serialize the original response message to JSON
2. If the serialized length exceeds the threshold (40KB), proceed with chunking
3. Encode the JSON string as UTF-8, then Base64 encode it
4. Split the Base64 string into chunks of the maximum chunk size
5. Generate a unique `messageId` (UUID recommended)
6. Send each chunk as a separate CHUNK response event
Example encoding (JavaScript):
```javascript
const encoded = btoa(unescape(encodeURIComponent(jsonString)))
```
#### Reassembly Process (Receiver)
1. When receiving a CHUNK response, buffer it by `messageId`
2. Track received chunks by `index`
3. When all chunks are received (`chunks.size === total`):
a. Concatenate chunk data in index order (0, 1, 2, ...)
b. Base64 decode the concatenated string
c. Parse as UTF-8 JSON to recover the original response
4. Process the reassembled response as normal
5. Clean up the chunk buffer
Example decoding (JavaScript):
```javascript
const jsonString = decodeURIComponent(escape(atob(concatenatedBase64)))
const response = JSON.parse(jsonString)
```
#### Chunk Buffer Management
Receivers MUST implement chunk buffer cleanup:
- Discard incomplete chunk buffers after 60 seconds of inactivity
- Limit the number of concurrent incomplete messages to prevent memory exhaustion
- Log warnings when discarding stale buffers for debugging
#### Ordering and Reliability
- Chunks MAY arrive out of order; receivers MUST reassemble by index
- Missing chunks result in message loss; the incomplete buffer is eventually discarded
- Duplicate chunks (same messageId + index) SHOULD be ignored
- Each chunk is sent as a separate encrypted NRC response event
### Authentication
#### Secret-Based Authentication
1. Client derives a keypair from the `secret` parameter in the URI
2. Client signs all request events with this derived key
3. Bridge verifies the client's pubkey is in its authorized list
4. Conversation key provides implicit authentication (only authorized clients can decrypt responses)
#### CAT Token Authentication
1. Client obtains a CAT token from the relay's mint with scope `nrc`
2. Token is bound to client's Nostr pubkey
3. Client includes token in the `cashu` tag of request events
4. Bridge verifies token signature and scope
5. Bridge re-authorizes via ACL on each request (enables immediate revocation)
### Access Revocation
**Secret-based**: Remove the client's derived pubkey from the authorized list.
**CAT-based**: Remove the client's Nostr pubkey from the ACL. Takes effect immediately due to re-authorization on each request.
## Security Considerations
1. **End-to-end encryption**: The rendezvous relay cannot read tunneled messages
2. **Perfect forward secrecy**: Not provided; if secret is compromised, past messages can be decrypted
3. **Rate limiting**: Bridges SHOULD enforce rate limits to prevent abuse
4. **Session expiry**: Sessions SHOULD timeout after a period of inactivity
5. **TLS**: The rendezvous relay connection SHOULD use TLS (wss://)
6. **Secret storage**: Clients SHOULD store connection URIs securely (they contain secrets)
## Client Implementation Notes
1. Generate a random session ID on connection
2. Subscribe to kind 24892 events with `#p` filter for client's pubkey
3. For each outgoing message, wrap in kind 24891 and publish
4. Match responses using the `e` tag (references request event ID)
5. Handle EOSE by waiting for kind 24892 with type "EOSE" in content
6. For subscriptions, maintain mapping of internal sub IDs to tunnel session
7. **Chunking**: Maintain a chunk buffer map keyed by `messageId`
8. **Chunking**: When receiving CHUNK responses, buffer chunks and reassemble when complete
9. **Chunking**: Implement 60-second timeout for incomplete chunk buffers
## Bridge Implementation Notes
1. Subscribe to kind 24891 events with `#p` filter for relay's pubkey
2. Verify client authorization (secret-based or CAT)
3. Decrypt content and forward to local relay via internal WebSocket
4. Capture all relay responses and wrap in kind 24892
5. Sign with relay's key and publish to rendezvous relay
6. Maintain session state for subscription mapping
7. **Chunking**: Check response size before sending; chunk if > 40KB
8. **Chunking**: Use consistent messageId (UUID) across all chunks of a message
9. **Chunking**: Send chunks in order (index 0, 1, 2, ...) for optimal reassembly
## Reference Implementations
- ORLY Relay (Bridge): [https://git.mleku.dev/mleku/next.orly.dev](https://git.mleku.dev/mleku/next.orly.dev)
- Smesh Client: [https://git.mleku.dev/mleku/smesh](https://git.mleku.dev/mleku/smesh)
## See Also
- [NIP-44: Encrypted Payloads](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [NIP-47: Nostr Wallet Connect](https://github.com/nostr-protocol/nips/blob/master/47.md)
- [NIP-46: Nostr Remote Signing](https://github.com/nostr-protocol/nips/blob/master/46.md)