diff --git a/app/config/config.go b/app/config/config.go index 8fceeba..9c8852c 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -52,6 +52,7 @@ type C struct { RelayAddresses []string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of websocket addresses for this relay (e.g., wss://relay.example.com,wss://backup.example.com)"` RelayPeers []string `env:"ORLY_RELAY_PEERS" usage:"comma-separated list of peer relay URLs for distributed synchronization (e.g., https://peer1.example.com,https://peer2.example.com)"` RelayGroupAdmins []string `env:"ORLY_RELAY_GROUP_ADMINS" usage:"comma-separated list of npubs authorized to publish relay group configuration events"` + ClusterAdmins []string `env:"ORLY_CLUSTER_ADMINS" usage:"comma-separated list of npubs authorized to manage cluster membership"` FollowListFrequency time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" usage:"how often to fetch admin follow lists (default: 1h)" default:"1h"` // Blossom blob storage service level settings diff --git a/app/handle-event.go b/app/handle-event.go index e6fdc48..0ea0f4e 100644 --- a/app/handle-event.go +++ b/app/handle-event.go @@ -467,6 +467,13 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { } } + // Handle cluster membership events (Kind 39108) + if env.E.Kind == 39108 && l.clusterManager != nil { + if err := l.clusterManager.HandleMembershipEvent(env.E); err != nil { + log.W.F("invalid cluster membership event %s: %v", hex.Enc(env.E.ID), err) + } + } + // Update serial for distributed synchronization if l.syncManager != nil { l.syncManager.UpdateSerial() diff --git a/app/main.go b/app/main.go index 24fa747..8cf7e9a 100644 --- a/app/main.go +++ b/app/main.go @@ -152,6 +152,23 @@ func Run( } } + // Initialize cluster manager for cluster replication + var clusterAdminNpubs []string + if len(cfg.ClusterAdmins) > 0 { + clusterAdminNpubs = cfg.ClusterAdmins + } else { + // Default to regular admins if no cluster admins specified + for _, admin := range cfg.Admins { + clusterAdminNpubs = append(clusterAdminNpubs, admin) + } + } + + if len(clusterAdminNpubs) > 0 { + l.clusterManager = dsync.NewClusterManager(ctx, db, clusterAdminNpubs) + l.clusterManager.Start() + log.I.F("cluster replication manager initialized with %d admin npubs", len(clusterAdminNpubs)) + } + // Initialize the user interface l.UserInterface() diff --git a/app/server.go b/app/server.go index dd153e0..fd49558 100644 --- a/app/server.go +++ b/app/server.go @@ -53,6 +53,7 @@ type Server struct { spiderManager *spider.Spider syncManager *dsync.Manager relayGroupMgr *dsync.RelayGroupManager + clusterManager *dsync.ClusterManager blossomServer *blossom.Server } @@ -259,6 +260,13 @@ func (s *Server) UserInterface() { s.mux.HandleFunc("/blossom/", s.blossomHandler) log.Printf("Blossom blob storage API enabled at /blossom") } + + // Cluster replication API endpoints + if s.clusterManager != nil { + s.mux.HandleFunc("/cluster/latest", s.clusterManager.HandleLatestSerial) + s.mux.HandleFunc("/cluster/events", s.clusterManager.HandleEventsRange) + log.Printf("Cluster replication API enabled at /cluster") + } } // handleFavicon serves orly-favicon.png as favicon.ico diff --git a/docs/NIP-XX-Cluster-Replication.md b/docs/NIP-XX-Cluster-Replication.md new file mode 100644 index 0000000..e49244c --- /dev/null +++ b/docs/NIP-XX-Cluster-Replication.md @@ -0,0 +1,322 @@ +NIP-XX +====== + +Cluster Replication Protocol +---------------------------- + +`draft` `optional` + +## Abstract + +This NIP defines an HTTP-based pull replication protocol for relay clusters. It enables relay operators to form distributed networks where relays actively poll each other to synchronize events, providing efficient traffic patterns and improved data availability. Cluster membership is managed by designated cluster administrators who publish membership lists that relays replicate and use to update their polling targets. + +## Motivation + +Current Nostr relay implementations operate independently, leading to fragmented event storage across the network. Users must manually configure multiple relays to ensure their events are widely available. This creates several problems: + +1. **Event Availability**: Important events may not be available on all relays a user wants to interact with +2. **Manual Synchronization**: Users must manually publish events to multiple relays +3. **Discovery Issues**: Clients have difficulty finding complete event histories +4. **Resource Inefficiency**: Relays store duplicate events without coordination +5. **Network Fragmentation**: Related events become scattered across disconnected relays + +This NIP addresses these issues by enabling relay operators to form clusters that actively replicate events using efficient HTTP polling mechanisms, creating more resilient and bandwidth-efficient event distribution networks. + +## Specification + +### Event Kinds + +This NIP defines the following new event kinds: + +| Kind | Description | +|------|-------------| +| `39108` | Cluster Membership List | + +### Cluster Membership List (Kind 39108) + +Cluster administrators publish this replaceable event to define the current set of cluster members. All cluster relays replicate this event and update their polling lists when it changes: + +```json +{ + "kind": 39108, + "content": "{\"name\":\"My Cluster\",\"description\":\"Community relay cluster\",\"admins\":[\"npub1...\",\"npub2...\"]}", + "tags": [ + ["d", "membership"], + ["relay", "https://relay1.example.com/", "wss://relay1.example.com/"], + ["relay", "https://relay2.example.com/", "wss://relay2.example.com/"], + ["relay", "https://relay3.example.com/", "wss://relay3.example.com/"], + ["admin", "npub1admin..."], + ["admin", "npub1admin2..."], + ["version", "1"] + ], + "pubkey": "", + "created_at": , + "id": "", + "sig": "" +} +``` + +**Tags:** +- `d`: Identifier for the membership list (always "membership") +- `relay`: HTTP and WebSocket URLs of cluster member relays (comma-separated) +- `admin`: npub of cluster administrator (can have multiple) +- `version`: Protocol version number + +**Content:** JSON object containing cluster metadata (name, description, admin list) + +**Authorization:** Only events signed by cluster administrators (listed in `admin` tags) are valid for membership updates. + +### HTTP API Endpoints + +#### 1. Latest Serial Endpoint + +Returns the current highest event serial number in the relay's database. + +**Endpoint:** `GET /cluster/latest` + +**Response:** +```json +{ + "serial": 12345678, + "timestamp": 1640995200 +} +``` + +**Parameters:** +- `serial`: The highest event serial number in the database +- `timestamp`: Unix timestamp when this serial was last updated + +#### 2. Event IDs by Serial Range Endpoint + +Returns event IDs for a range of serial numbers. + +**Endpoint:** `GET /cluster/events` + +**Query Parameters:** +- `from`: Starting serial number (inclusive) +- `to`: Ending serial number (inclusive) +- `limit`: Maximum number of event IDs to return (default: 1000, max: 10000) + +**Response:** +```json +{ + "events": [ + { + "serial": 12345670, + "id": "abc123...", + "timestamp": 1640995100 + }, + { + "serial": 12345671, + "id": "def456...", + "timestamp": 1640995110 + } + ], + "has_more": false, + "next_from": null +} +``` + +**Response Fields:** +- `events`: Array of event objects with serial, id, and timestamp +- `has_more`: Boolean indicating if there are more results +- `next_from`: Serial number to use as `from` parameter for next request (if `has_more` is true) + +### Replication Protocol + +#### 1. Cluster Discovery + +1. Cluster administrators publish Kind 39108 events defining cluster membership +2. Relays configured with cluster admin npubs subscribe to these events +3. When membership updates are received, relays update their polling lists +4. Polling begins immediately with 5-second intervals to all listed relays + +#### 2. Active Replication Process + +Each relay maintains a replication state for each cluster peer: + +1. **Poll Latest Serial**: Every 5 seconds, query `/cluster/latest` from each peer +2. **Compare Serials**: If peer has higher serial than local replication state, fetch missing events +3. **Fetch Event IDs**: Use `/cluster/events` to get event IDs in the serial range gap +4. **Fetch Full Events**: Use standard WebSocket REQ messages to get full event data +5. **Store Events**: Validate and store events in local database (relays MAY choose not to store every event they receive) +6. **Update State**: Record the highest successfully replicated serial for each peer + +#### 3. Serial Number Management + +Each relay maintains an internal serial number that increments with each stored event: + +- **Serial Assignment**: Events are assigned serial numbers in the order they are stored +- **Monotonic Increase**: Serial numbers only increase, never decrease +- **Gap Handling**: Missing serials are handled gracefully +- **Peer State Tracking**: Each relay tracks the last replicated serial from each peer +- **Restart Recovery**: On restart, relays load persisted serial state and resume replication from the last processed serial + +#### 4. Conflict Resolution + +When fetching events that already exist locally: + +1. **Serial Consistency**: If serial numbers match, events should be identical +2. **Timestamp Priority**: For conflicting events, newer timestamps take precedence +3. **Signature Verification**: Invalid signatures always result in rejection +4. **Author Authority**: Original author events override third-party copies +5. **Event Kind Rules**: Follow NIP-01 replaceable event semantics where applicable + +## Message Flow Examples + +### Basic Replication Flow + +``` +Relay A Relay B + | | + |--- User Event ---------->| (Event stored with serial 1001) + | | + | | (5 seconds later) + | | + |<--- GET /cluster/latest --| (A polls B, gets serial 1001) + |--- Response: 1001 ------->| + | | + |<--- GET /cluster/events --| (A fetches event IDs from serial 1000-1001) + |--- Response: [event_id] ->| + | | + |<--- REQ [event_id] ------| (A fetches full event via WebSocket) + |--- EVENT [event_id] ---->| + | | + | (Event stored locally) | +``` + +### Cluster Membership Update Flow + +``` +Admin Client Relay A Relay B + | | | + |--- Kind 39108 -------->| (New member added) | + | | | + | |<--- REQ membership ----->| (A subscribes to membership updates) + | |--- EVENT membership ---->| + | | | + | | (A updates polling list)| + | | | + | |<--- GET /cluster/latest -| (A starts polling B) + | | | +``` + +## Security Considerations + +1. **Administrator Authorization**: Only cluster administrators can modify membership lists +2. **Transport Security**: HTTP endpoints SHOULD use HTTPS for secure communication +3. **Rate Limiting**: Implement rate limiting on polling endpoints to prevent abuse +4. **Event Validation**: All fetched events MUST be fully validated before storage +5. **Access Control**: HTTP endpoints SHOULD implement proper access controls +6. **Privacy**: Membership lists contain relay addresses but no sensitive user data +7. **Audit Logging**: All replication operations SHOULD be logged for monitoring +8. **Network Isolation**: Clusters SHOULD be isolated from public relay operations +9. **Serial Consistency**: Serial numbers help detect tampering or data corruption + +## Implementation Guidelines + +### Relay Operators + +1. Configure cluster administrator npubs to monitor membership updates +2. Implement HTTP endpoints for `/cluster/latest` and `/cluster/events` +3. Set up 5-second polling intervals to all cluster peers +4. Implement peer state persistence to track last processed serials +5. Monitor replication health and alert on failures +6. Handle cluster membership changes gracefully (cleaning up removed peer state) +7. Implement proper serial number management +8. Document cluster configuration + +### Client Developers + +1. Clients MAY display cluster membership information for relay discovery +2. Clients SHOULD prefer cluster relays for improved event availability +3. Clients can use membership events to find additional relay options +4. Clients SHOULD handle relay failures within clusters gracefully + +## Backwards Compatibility + +This NIP is fully backwards compatible: + +- Relays not implementing this NIP continue to operate normally +- The HTTP endpoints are optional additions to existing relay functionality +- Standard WebSocket event fetching continues to work unchanged +- Users can continue using relays without cluster participation +- Existing event kinds and message types are unchanged + +## Reference Implementation + +A reference implementation SHOULD include: + +1. HTTP endpoint handlers for `/cluster/latest` and `/cluster/events` +2. Cluster membership subscription and parsing logic +3. Replication polling scheduler with 5-second intervals +4. Serial number management and tracking +5. Peer state persistence and recovery (last known serials stored in database) +6. Peer state management and failure handling +7. Configuration management for cluster settings + +## Test Vectors + +### Example Membership Event + +```json +{ + "kind": 39108, + "content": "{\"name\":\"Test Cluster\",\"description\":\"Development cluster\",\"admins\":[\"npub1testadmin1\",\"npub1testadmin2\"]}", + "tags": [ + ["d", "membership"], + ["relay", "https://relay1.test.com/", "wss://relay1.test.com/"], + ["relay", "https://relay2.test.com/", "wss://relay2.test.com/"], + ["admin", "npub1testadmin1"], + ["admin", "npub1testadmin2"], + ["version", "1"] + ], + "pubkey": "testadminpubkeyhex", + "created_at": 1640995200, + "id": "membership_event_id", + "sig": "membership_event_signature" +} +``` + +### Example Latest Serial Response + +```json +{ + "serial": 12345678, + "timestamp": 1640995200 +} +``` + +### Example Events Range Response + +```json +{ + "events": [ + { + "serial": 12345676, + "id": "event_id_1", + "timestamp": 1640995190 + }, + { + "serial": 12345677, + "id": "event_id_2", + "timestamp": 1640995195 + }, + { + "serial": 12345678, + "id": "event_id_3", + "timestamp": 1640995200 + } + ], + "has_more": false, + "next_from": null +} +``` + +## Changelog + +- 2025-01-XX: Initial draft + +## Copyright + +This document is placed in the public domain. diff --git a/docs/NIP-XX-distributed-directory-consensus.md b/docs/NIP-XX-distributed-directory-consensus.md index 36160fb..19a90db 100644 --- a/docs/NIP-XX-distributed-directory-consensus.md +++ b/docs/NIP-XX-distributed-directory-consensus.md @@ -1,1078 +1,16 @@ -NIP-XX -====== +# DEPRECATED: This NIP has been replaced -Distributed Directory Consensus using Relay Identity Keys and Web of Trust ---------------------------------------------------------------------------- +**This document has been superseded by [NIP-XX-Cluster-Replication.md](NIP-XX-Cluster-Replication.md)** -`draft` `optional` +The distributed directory consensus protocol described in this document has been replaced by the HTTP-based pull replication protocol defined in the Cluster Replication NIP. The new protocol provides more efficient traffic patterns through active polling rather than push-based event replication. -## Abstract +## Migration -This NIP defines a protocol for distributed consensus among Nostr relays using replica identity keys and a web of trust mechanism to regulate replication of directory events. It enables relay operators to form trusted consortiums that automatically synchronize essential identity-related events (metadata, follow lists, relay lists, mute lists) while maintaining decentralization and Byzantine fault tolerance. +If you were implementing or using the protocol described in this document: -## Motivation +1. **Stop using** the WebSocket event-based replication (Kinds 39100-39112) +2. **Switch to** the HTTP polling-based cluster replication protocol +3. **Update membership management** to use Kind 39108 events published by cluster administrators +4. **Configure polling** to use the `/cluster/latest` and `/cluster/events` HTTP endpoints -Current Nostr relay implementations operate independently, leading to fragmentation of user directory information across the network. Users must manually configure multiple relays to ensure their profile data and social graph information is widely available. This creates several problems: - -1. **Data Availability**: Essential user directory events may not be available on all relays a user wants to interact with -2. **Synchronization Overhead**: Users must manually publish directory events to multiple relays -3. **Discovery Issues**: New users have difficulty finding existing users and their current relay preferences -4. **Trust Management**: No standardized way for relay operators to establish trusted relationships for data sharing - -This NIP addresses these issues by enabling relay operators to form trusted consortiums that automatically replicate directory events among trusted peers, similar to the democratic Byzantine Fault Tolerant approach used in [pnyxdb](https://github.com/technicolor-research/pnyxdb). - -## Specification - -### Relay Identity Keys - -Each participating relay MUST generate and maintain a long-term identity keypair separate from any user keys: - -- **Identity Key**: A secp256k1 keypair used to identify the relay in the consortium. The public key MUST be listed in the `pubkey` field of the NIP-11 relay information document, and the relay MUST prove control of the corresponding private key through the signature mechanism described below. -- **Signing Keys**: secp256k1 keys used for Schnorr signatures on acts and directory events -- **Encryption Keys**: secp256k1 keys used for ECDH encryption of sensitive consortium communications - -The relay identity key serves as the authoritative identifier for the relay and MUST be discoverable through the standard NIP-11 relay information document available at `https:///.well-known/nostr.json` or via the `NIP11` WebSocket message. This ensures that any client or relay can verify the identity of a consortium member by requesting their relay information document and comparing the public key. - -### NIP-11 Extensions for Identity Verification - -This protocol extends the NIP-11 relay information document with two additional fields to prove control of the advertised public key: - -```json -{ - "name": "relay.example.com", - "description": "A community relay", - "pubkey": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", - "contact": "admin@example.com", - "supported_nips": [1, 2, 9, 11, 12, 15, 16, 20, 22], - "software": "https://github.com/example/relay", - "version": "1.0.0", - "nonce": "a1b2c3d4e5f6789012345678901234567890abcdef", - "sig": "3045022100ab1234...def567890123456789012345678901234567890abcdef" -} -``` - -**New Fields:** -- `nonce`: A random hex-encoded value (recommended 20+ bytes) used to strengthen signature security -- `sig`: A secp256k1 signature proving control of the private key corresponding to `pubkey` - -**Signature Generation:** -1. Concatenate the `pubkey`, `nonce`, and relay address as strings: `pubkey + nonce + relay_address` -2. The relay address MUST be the canonical WebSocket URL (e.g., "wss://relay.example.com/", note the path suffix) -3. Compute SHA256 hash of the concatenated string -4. Sign the hash using the relay's private key (corresponding to `pubkey`) -5. Encode the signature as hex and store in the `sig` field - -**Verification Process:** -1. Extract `pubkey`, `nonce`, and `sig` from the NIP-11 document -2. Determine the relay address from the request URL or connection context -3. Concatenate `pubkey + nonce + relay_address` as strings -4. Compute SHA256 hash of the concatenated string -5. Verify the signature using the public key and computed hash -6. If verification succeeds, the relay has proven control of the private key AND binding to the network address - -This mechanism ensures that only the entity controlling the private key can generate a valid NIP-11 document for a specific network address, preventing relay impersonation attacks where an attacker might copy another relay's public key without controlling the corresponding private key. The inclusion of the relay address in the signature prevents an attacker from copying a valid NIP-11 document and hosting it at a different address. - -### Event Kinds - -This NIP defines the following new event kinds: - -| Kind | Description | -|------|-------------| -| `39100` | Relay Identity Announcement | -| `39101` | Trust Act | -| `39102` | Group Tag Act (Registration) | -| `39103` | Public Key Advertisement | -| `39104` | Directory Event Replication Request | -| `39105` | Directory Event Replication Response | -| `39106` | Group Tag Transfer (Ownership Transfer) | -| `39107` | Escrow Witness Completion Act | - -### Relay Identity Announcement (Kind 39100) - -Relay operators publish this event to announce their participation in the distributed directory consortium. This event MUST be signed with the same private key used to sign the relay's NIP-11 information document: - -```json -{ - "kind": 39100, - "content": "{\"name\":\"relay.example.com\",\"description\":\"Community relay\",\"contact\":\"admin@example.com\"}", - "tags": [ - ["d", "relay-identity"], - ["relay", "wss://relay.example.com/"], - ["signing_key", ""], - ["encryption_key", ""], - ["version", "1"] - ] -} -``` - -**Tags:** -- `d`: Identifier for the relay identity (always "relay-identity") -- `relay`: WebSocket URL of the relay -- `signing_key`: Public key for verifying acts from this relay (MAY be the same as identity key) -- `encryption_key`: Public key for ECDH encryption -- `version`: Protocol version number - -**Identity Verification Process:** -1. Other relays receive this announcement event -2. They extract the `pubkey` field (relay identity key) and `relay` URL -3. They fetch the NIP-11 document by making an HTTP GET request to the relay URL with the `Accept: application/nostr+json` header - - For `wss://relay.example.com/` → HTTP GET `https://relay.example.com/` with header `Accept: application/nostr+json` - - For `ws://relay.example.com/` → HTTP GET `http://relay.example.com/` with header `Accept: application/nostr+json` -4. They verify the NIP-11 signature using the extended verification process: - - Extract `pubkey`, `nonce`, and `sig` from the NIP-11 document - - Verify that the `pubkey` matches the announcement event's `pubkey` - - Extract the relay address from the `relay` tag - - Concatenate `pubkey + nonce + relay_address` and compute SHA256 hash - - Verify the signature proves control of the private key AND address binding -5. They verify that the announcement event is signed by the same key -6. This confirms that the relay identity is cryptographically bound to the specific network address - -### Trust Act (Kind 39101) - -Relay operators create trust acts toward other relays they wish to enter consensus with: - -```json -{ - "kind": 39101, - "content": "", - "tags": [ - ["p", ""], - ["trust_level", "75"], - ["relay", ""], - ["expiry", ""], - ["reason", "manual|automatic|inherited"], - ["K", "1,3,6,7,1984,30023"], - ["I", "", "", ""] - ] -} -``` - -**Tags:** -- `p`: Public key of the target relay being attested -- `trust_level`: Replication percentage (0-100) indicating probability of replicating each event -- `relay`: WebSocket URL of the target relay -- `expiry`: Optional expiration timestamp for the act -- `reason`: How this trust relationship was established -- `K`: Comma-separated list of event kinds to replicate in near real-time (in addition to directory events) -- `I`: Identity tag with npub, nonce, and proof-of-control signature (same format as Kind 39103) - -**Trust Level (Partial Replication):** - -The `trust_level` is a number from **0 to 100** representing the **percentage probability** that any given event will be replicated. This implements **partial replication** where events are randomly selected based on a dice-throw mechanism: - -- **100**: Full replication - ALL events replicated (100% probability) -- **75**: High partial replication - 75% of events replicated on average -- **50**: Medium partial replication - 50% of events replicated on average -- **25**: Low partial replication - 25% of events replicated on average -- **10**: Minimal partial replication - 10% of events replicated on average -- **0**: No replication - effectively disables replication - -**Partial Replication Mechanism:** - -For each event received from a trusted relay: - -1. **Generate Random Number**: Create a cryptographically secure random number between 0-100 -2. **Compare to Threshold**: If random number ≤ trust_level, replicate the event -3. **Otherwise Skip**: If random number > trust_level, discard without replication -4. **Per-Event Decision**: Each event gets an independent random roll - -**Example:** -``` -Trust Level: 50 -Event A arrives → Roll: 42 → 42 ≤ 50 → REPLICATE -Event B arrives → Roll: 73 → 73 > 50 → SKIP -Event C arrives → Roll: 18 → 18 ≤ 50 → REPLICATE -Event D arrives → Roll: 91 → 91 > 50 → SKIP - -Result: ~50% of events replicated over time -``` - -**Partial Replication Benefits:** - -1. **Resource Management**: Reduce bandwidth, storage, and processing load proportionally -2. **Probabilistic Coverage**: Events still propagate through the network via multiple paths -3. **Network Resilience**: Different relays replicate different random subsets, providing redundancy -4. **Tunable Trade-offs**: Operators can precisely balance resources vs. completeness -5. **Graceful Degradation**: Network remains functional even with low trust levels across many peers - -**Network Propagation Example:** - -Consider a network where all relays use 50% trust level: -``` -User publishes Event X - ↓ -Relay A receives → 50% chance → Replicates to Relays B, C, D - ↓ -Relay B (rolled yes) → 50% chance → Replicates to Relays E, F -Relay C (rolled no) → Skips -Relay D (rolled yes) → 50% chance → Replicates to Relays G, H - -Result: Event X reaches ~75-85% of network despite 50% replication rate - (due to multiple propagation paths) -``` - -**Trust Level Guidelines:** - -- **90-100**: Use for critical partners where near-complete coverage is essential - - Primary consortium members - - Paid backup services - - Legal/compliance requirements - -- **60-89**: Use for important partners with good resources - - Secondary consortium members - - Established relay partnerships - - Balanced resource/coverage trade-off - -- **30-59**: Use for standard partnerships - - General peer relationships - - Resource-constrained but willing participants - - Acceptable coverage with good bandwidth savings - -- **10-29**: Use for exploratory or limited partnerships - - New/untrusted relays being evaluated - - Severely resource-constrained peers - - Experimental connectivity - -- **1-9**: Use for minimal sampling - - Network topology discovery - - Quality/spam assessment - - Proof-of-concept testing - -**Implementation Requirements:** - -Relays implementing partial replication MUST: - -1. **Use Cryptographic RNG**: Use cryptographically secure random number generation (e.g., `crypto/rand` in Go, `crypto.getRandomValues()` in JavaScript) -2. **Per-Event Independence**: Each event must get an independent random roll -3. **No Bias**: Random selection must be uniform and unbiased -4. **Deterministic Recording**: Once decision is made, it must be consistent (no re-rolling) -5. **Event Integrity**: Replicated events must be complete and unmodified - -**Optional Enhancements:** - -Relays MAY implement additional strategies: - -- **Priority Boosting**: Increase probability for directory events (Kinds 0, 3, 5, etc.) -- **Kind-Specific Rates**: Apply different trust levels to different event kinds -- **Time-Based Adjustment**: Vary trust level based on network load or time of day -- **Reputation Weighting**: Boost probability for high-reputation users - -However, the base trust_level percentage MUST always be respected as the minimum probability. - -**Event Kind Replication:** -- **Directory Events**: Always replicated regardless of `K` tag (kinds 0, 3, 5, 1984, 10002, 10000, 10050) -- **Custom Kinds**: Additional event kinds specified in the `K` tag are replicated based on trust level -- **Specialization**: Enables relay operators to specialize in specific data types (e.g., long-form content, marketplace events, etc.) -- **Near Real-time**: Events matching `K` tag kinds are replicated with minimal delay -- **Bidirectional**: Replication occurs both to and from the trusted relay for specified kinds - -### Group Tag Act (Kind 39102) - -Group Tag Acts establish ownership and control over arbitrary string tags, functioning as a first-come-first-served registration system akin to domain name registration. These tags form the foundation for permissioned structured groups that can span multiple relays while maintaining consistent state. They also constitute a name registration system akin to DNS, and could have IP adress routing information attached to it with events (best to be CRDT add/remove so it's idempotent). - -Since the uses of group identity names and webserver identity information (and it logically would include most of the same things you find in DNS records) tend to overlap, in that finding the group and going to their static content (such as structured long form documents and wikis) is the same place you want to go to read public messages and discover new friends. This allows progressive replication and redundancy for communities that enables the network to scale to larger userbases without a linear reduction in performance. - -**Registration Model:** -- Group tags are **alienable** (transferable) and follow first-come-first-served registration -- The first valid Group Tag Act for a given `group_id` establishes initial ownership -- Ownership can be transferred through Group Tag Transfer events (Kind 39106) with optional escrow -- Multiple signature schemes are supported for ownership control -- Group tag names MUST use URL-safe character set (RFC 3986) - -**Group Tag Naming Rules:** - -Group tag identifiers MUST conform to URL path segment rules (RFC 3986): -- **Allowed characters**: `a-z`, `A-Z`, `0-9`, `-`, `.`, `_`, `~` -- **Forbidden characters**: `/`, `?`, `#`, `[`, `]`, `@`, `!`, `$`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `;`, `=`, `:`, spaces -- **Length**: 1-255 characters -- **Case sensitivity**: Group tags are case-sensitive -- **Reserved prefixes**: Tags starting with `.` or `_` are reserved for system use - -**Examples:** -- ✅ Valid: `bitcoin-discussion`, `nostr.community`, `tech_forum`, `cafe~network` -- ❌ Invalid: `bitcoin/discussion`, `nostr community`, `tech#forum`, `café:network` - -```json -{ - "kind": 39102, - "content": "", - "tags": [ - ["d", ""], - ["group_tag", "", ""], - ["actor", ""], - ["confidence", "0-100"], - ["owners", "", "", "", "..."], - ["created", ""], - ["I", "", "", ""] - ] -} -``` - -**Tags:** -- `d`: Unique identifier for this group (the registered tag name, must be URL-safe) -- `group_tag`: The tag name and value being registered -- `actor`: Public key of the relay making the act -- `confidence`: Confidence level (0-100) in this act -- `owners`: Ownership control specification (see below) -- `created`: Timestamp of group registration -- `I`: Identity tag for proof-of-control (optional) - -**Ownership Schemes:** - -The `owners` tag specifies the signature requirements for group control: - -1. **Single Signature:** - ``` - ["owners", "single", ""] - ``` - - Only one signature required for group operations - - Simplest ownership model - -2. **2-of-3 Multisig:** - ``` - ["owners", "2-of-3", "", "", ""] - ``` - - Requires 2 out of 3 owners to sign for group operations - - Provides redundancy and shared control - -3. **3-of-5 Multisig:** - ``` - ["owners", "3-of-5", "", "", "", "", ""] - ``` - - Requires 3 out of 5 owners to sign for group operations - - Maximum distributed control while maintaining operational flexibility - -**Use Cases:** - -Group Tag Acts enable various structured group scenarios: - -1. **Forum/Community Groups:** - - Register a group tag like "bitcoin-discussion" - - Anchor posts, moderation actions, and membership lists to this tag - - Users only need one working relay from the group's relay set to see current state - -2. **Permissioned Content Collections:** - - Create private or curated content spaces - - Owner(s) control membership and posting permissions - - Content can be distributed across multiple relays - -3. **Multi-Relay Coordination:** - - Group state spans multiple relays in the consortium - - Users see consistent group state regardless of which relay they connect to - - Ownership transfers maintain continuity across relay set - -4. **Administrative Hierarchies:** - - Multisig ownership enables distributed administration - - 2-of-3 or 3-of-5 schemes prevent single-point-of-failure - - Ownership can be transferred to new administrator sets - -5. **Domain Name Service Replacement:** - - Additional event kinds could be created that specify such things as a set of IP addresses that have servers that are replicas of the group or individual that owns and operates the relays. - - Additional types of services could be delivered, such as compositing compound multi-event document types into structured document formats for reading, so, subprotocols, like the differences between FTP and Gopher and HTTP, so the name service events can also include public metadata about the servers such as operating ports and the protocols available through them. - -**Registration Rules:** - -- **First Registration Wins:** The first valid Group Tag Act for a given `d` (group identifier) establishes ownership -- **Timestamp Precedence:** If multiple registration attempts occur simultaneously, earliest `created_at` wins -- **Conflict Resolution:** Relays MUST reject later registration attempts for the same group identifier -- **Transfer Authority:** Only current owners (with valid signatures) can transfer ownership - -### Group Tag Transfer (Kind 39106) - -Group Tag Transfer events enable ownership transfer of registered group tags, functioning as a "deed of sale" mechanism. Transfers can be executed immediately or through a witness-based escrow process. - -**Direct Transfer (No Escrow):** - -```json -{ - "kind": 39106, - "content": "", - "tags": [ - ["d", ""], - ["from_owners", "", "", "..."], - ["to_owners", "", "", "..."], - ["transfer_date", ""], - ["signatures", "", "", "..."], - ["I", "", "", ""] - ] -} -``` - -**Escrow Transfer (With Witnesses):** - -```json -{ - "kind": 39106, - "content": "", - "tags": [ - ["d", ""], - ["from_owners", "", "", "..."], - ["to_owners", "", "", "..."], - ["transfer_date", ""], - ["escrow_id", ""], - ["seller_witness", ""], - ["buyer_witness", ""], - ["conditions", ""], - ["signatures", "", "", "..."], - ["I", "", "", ""] - ] -} -``` - -**Tags:** -- `d`: The group identifier being transferred (must match existing Group Tag Act) -- `from_owners`: Current ownership specification (must match existing Group Tag Act) -- `to_owners`: New ownership specification after transfer -- `transfer_date`: When the transfer takes effect -- `escrow_id`: Unique identifier for this escrow transaction (required for escrow) -- `seller_witness`: Pubkey of witness designated by seller (required for escrow) -- `buyer_witness`: Pubkey of witness designated by buyer (required for escrow) -- `conditions`: SHA256 hash of transfer conditions document (required for escrow) -- `signatures`: Schnorr signatures from current owners (must meet threshold) -- `I`: Identity tag for proof-of-control (optional) - -**Transfer Validation:** - -Relays MUST validate transfers according to these rules: - -1. **Group Existence:** A Group Tag Act (Kind 39102) must exist for the specified `d` identifier -2. **Owner Match:** The `from_owners` tag must exactly match the current `owners` tag in the Group Tag Act -3. **Signature Threshold:** - - Single: 1 signature from the owner - - 2-of-3: 2 signatures from any of the 3 owners - - 3-of-5: 3 signatures from any of the 5 owners -4. **Signature Verification:** All provided signatures must be valid Schnorr signatures -5. **Chronological Order:** `transfer_date` must be after the group's `created` timestamp -6. **Escrow Validation (if applicable):** - - Both `seller_witness` and `buyer_witness` must be specified - - `escrow_id` must be unique and not previously used - - `conditions` hash must be present - - Transfer is PENDING until both witnesses sign completion acts - -**Signature Generation:** - -For each owner signing the transfer: - -1. Concatenate: `group_id + from_owners_json + to_owners_json + transfer_date` -2. Compute SHA256 hash of the concatenated string -3. Sign the hash using the owner's private key -4. Add signature to the `signatures` tag - -**Transfer Effect (Direct Transfer):** - -Once a valid non-escrow transfer is accepted: - -1. The Group Tag Act (Kind 39102) is considered superseded -2. A new implicit Group Tag Act with updated `owners` is recognized -3. All future group operations must use the new ownership specification -4. Old owners lose control; new owners gain full control immediately - -**Transfer Effect (Escrow Transfer):** - -When an escrow transfer is initiated: - -1. Transfer enters PENDING state -2. Old owners retain control until escrow completes -3. Witnesses must sign Escrow Witness Completion Acts (Kind 39107) -4. When BOTH witnesses sign, transfer completes automatically -5. New owners gain control; old owners lose control - -### Escrow Witness Completion Act (Kind 39107) - -Escrow witnesses publish completion acts to authorize the finalization of an escrow transfer. - -```json -{ - "kind": 39107, - "content": "", - "tags": [ - ["escrow_id", ""], - ["group_id", ""], - ["witness_role", "seller_witness|buyer_witness"], - ["completion_status", "approved|rejected"], - ["reason", ""], - ["verification_hash", ""], - ["timestamp", ""] - ] -} -``` - -**Tags:** -- `escrow_id`: The escrow transaction identifier (from Kind 39106) -- `group_id`: The group identifier being transferred -- `witness_role`: Role of this witness (`seller_witness` or `buyer_witness`) -- `completion_status`: Whether witness approves (`approved`) or rejects (`rejected`) -- `reason`: Optional explanation (required if rejected) -- `verification_hash`: SHA256 hash of conditions document witness verified -- `timestamp`: When witness completed verification - -**Escrow Protocol Flow:** - -1. **Initiation:** - - Seller creates Group Tag Transfer (Kind 39106) with escrow tags - - Both parties agree on witnesses and conditions - - Transfer enters PENDING state - -2. **Witness Designation:** - - Seller designates `seller_witness` (their chosen neutral party) - - Buyer designates `buyer_witness` (their chosen neutral party) - - Witnesses should be trusted relays or recognized arbiters - -3. **Condition Verification:** - - Witnesses independently verify transfer conditions are met - - Conditions document (hashed in `conditions` tag) specifies requirements - - Examples: payment received, ownership verified, legal requirements met - -4. **Witness Completion:** - - Each witness publishes Escrow Witness Completion Act (Kind 39107) - - Both witnesses must approve (`completion_status: approved`) - - If either witness rejects, transfer is cancelled - -5. **Transfer Finalization:** - - When BOTH witnesses sign with `approved` status - - Relays automatically recognize new owners - - HD keychains of new owners can modify group-tagged records - - Old owners immediately lose modification rights - -6. **Rejection Handling:** - - If ANY witness rejects (` completion_status: rejected`) - - Transfer is cancelled permanently - - Group remains with original owners - - New transfer must be initiated if parties wish to retry - -**Escrow Validation Rules:** - -Relays MUST validate escrow completion according to these rules: - -1. **Witness Verification:** - - Witness pubkey must match the designated witness in Kind 39106 - - Witness signature must be valid - - Witness event timestamp must be after transfer initiation - -2. **Escrow ID Matching:** - - `escrow_id` in Kind 39107 must match Kind 39106 - - `group_id` must match the transfer - -3. **Completion Requirements:** - - BOTH seller_witness AND buyer_witness must publish Kind 39107 - - BOTH must have `completion_status: approved` - - If ANY witness rejects, transfer fails - -4. **Condition Hash Verification:** - - `verification_hash` should match `conditions` hash from Kind 39106 - - Witnesses SHOULD verify conditions document before signing - -5. **Temporal Ordering:** - - Witness completion acts must come after transfer event - - Both completions must occur within reasonable timeframe (suggested: 30 days) - -**Witness Selection Guidelines:** - -Good witness candidates: -- Established relays in the consortium with high trust scores -- Professional escrow services recognized in the community -- Legal entities or notaries (for high-value transfers) -- Community-elected arbiters with reputation systems - -Poor witness candidates: -- Parties involved in the transfer (conflict of interest) -- Recently created identities with no history -- Witnesses with financial stake in the outcome - -**Example Transfer Flow:** - -**Direct Transfer (No Escrow):** -``` -Initial Registration (Kind 39102): - ["owners", "single", "alice-pubkey"] - -Direct Transfer (Kind 39106): - ["from_owners", "single", "alice-pubkey"] - ["to_owners", "2-of-3", "bob-pubkey", "carol-pubkey", "dave-pubkey"] - ["signatures", "alice-signature"] - # No escrow tags - -Result (Immediate): - Group now controlled by 2-of-3 multisig (Bob, Carol, Dave) -``` - -**Escrow Transfer:** -``` -Initial Registration (Kind 39102): - ["owners", "single", "alice-pubkey"] - Group: "bitcoin-marketplace" - -Escrow Transfer Initiated (Kind 39106): - ["from_owners", "single", "alice-pubkey"] - ["to_owners", "single", "bob-pubkey"] - ["escrow_id", "escrow-2024-001"] - ["seller_witness", "relay-witness-1-pubkey"] - ["buyer_witness", "relay-witness-2-pubkey"] - ["conditions", "sha256-of-payment-terms"] - ["signatures", "alice-signature"] - -Status: PENDING (Alice retains control) - -Seller's Witness Approves (Kind 39107): - ["escrow_id", "escrow-2024-001"] - ["witness_role", "seller_witness"] - ["completion_status", "approved"] - Signed by: relay-witness-1 - -Status: PENDING (Waiting for buyer's witness) - -Buyer's Witness Approves (Kind 39107): - ["escrow_id", "escrow-2024-001"] - ["witness_role", "buyer_witness"] - ["completion_status", "approved"] - Signed by: relay-witness-2 - -Result (Automatic upon both witnesses): - Group "bitcoin-marketplace" now controlled by Bob - Bob's HD keychain can modify all records tagged with this group - Alice's HD keychain loses modification rights -``` - -**Domain Name System Analogy:** - -Group Tag Acts function like DNS registrations: - -- **First-Come-First-Served:** Just as domain names are registered on a first-come basis -- **Alienable:** Can be sold, transferred, or reassigned like domain ownership -- **Globally Unique:** Each group identifier is unique within the consortium -- **Decentralized Registry:** Distributed across consortium relays instead of central authority -- **Transfer Mechanism:** Group Tag Transfer events are like domain transfer EPP codes -- **Multi-Relay Consistency:** Like DNS propagation, but consensus-based - -**Group State Coordination:** - -Groups span multiple relays while maintaining consistency: - -1. **Relay Set:** A group may be active on relays A, B, C, D -2. **User Connection:** User connects to relay B -3. **State Visibility:** User sees complete group state from all relays via replication -4. **Partial Connectivity:** Even if relays C and D are down, user sees state via A and B -5. **Ownership Operations:** Transfer events replicate across all relays in the set -6. **Consensus:** Relays validate ownership changes independently using same rules - -**Benefits:** - -- **Resilience:** Groups survive individual relay failures -- **Portability:** Users can switch between relays in the group's relay set -- **Consistency:** Ownership and membership state synchronized across relays -- **Flexibility:** Ownership can evolve from single to multisig to new parties -- **Transparency:** All transfers are publicly auditable on the relays - -### Hierarchical Deterministic Key Derivation - -This protocol uses BIP32-style HD key derivation to enable deterministic key generation and management across multiple clients sharing the same identity. - -**Derivation Path Structure:** -``` -m/purpose'/coin_type'/identity'/usage/index -``` - -Where: -- `purpose'`: 39103' (this NIP's purpose, hardened) -- `coin_type'`: 1237' (Nostr coin type, hardened) -- `identity'`: Identity index (0' for primary identity, hardened) -- `usage`: Key usage type (0=signing, 1=encryption, 2=delegation) - all secp256k1 -- `index`: Sequential key index (0, 1, 2, ...) - -**Example Derivation Paths:** -- `m/39103'/1237'/0'/0/0` - First secp256k1 signing key for primary identity -- `m/39103'/1237'/0'/1/0` - First secp256k1 encryption key (ECDH) for primary identity -- `m/39103'/1237'/0'/2/0` - First secp256k1 delegation key for primary identity - -**Seed Sharing Requirements:** -- Clients MUST use a secure side-channel to share the master seed (BIP39 mnemonic) -- The master seed enables all clients to derive the same key space -- Clients SHOULD use encrypted communication for seed distribution -- Seed rotation SHOULD be performed periodically for security - -### Public Key Advertisement (Kind 39103) - -Relays advertise public keys that will be used in future operations. Keys are derived using the HD scheme above. Each relay MUST limit the number of unused key delegations to 512 per identity. Key delegations expire after 30 days if not used in any database operations: - -```json -{ - "kind": 39103, - "content": "", - "tags": [ - ["d", ""], - ["pubkey", ""], - ["purpose", "signing|encryption|delegation"], - ["valid_from", ""], - ["valid_until", ""], - ["algorithm", "secp256k1"], - ["derivation_path", "m/39103'/1237'/0'/0/5"], - ["key_index", "5"], - ["I", "", "", ""] - ] -} -``` - -**Tags:** -- `d`: Unique identifier for this key advertisement -- `pubkey`: The public key being advertised (derived from HD path) -- `purpose`: Intended use of the key (signing, encryption, delegation) -- `valid_from`: When this key becomes valid -- `valid_until`: When this key expires -- `algorithm`: Cryptographic algorithm used (always "secp256k1") -- `derivation_path`: Full BIP32 derivation path used to generate this key -- `key_index`: The index component of the derivation path for easy reference -- `I`: Identity tag containing npub, nonce, and proof-of-control signature - -**Key Delegation Limits:** -- Maximum 512 unused key delegations per relay identity -- Key delegations expire after 30 days without database usage -- Expired delegations MUST be deleted to prevent unbounded growth -- Usage is defined as the key being referenced in any stored directory event - -**Identity Tag (`I`) Specification:** -The `I` tag provides an npub-encoded identity with proof of control that binds the identity to the delegate pubkey: - -1. **npub-identity**: The identity public key encoded in npub format (NIP-19) -2. **hex-nonce**: A random nonce (recommended 16+ bytes) encoded as hex -3. **hex-signature**: Signature proving the identity holder authorized the delegate pubkey - -**Identity Tag Signature Generation:** -1. Extract the delegate pubkey from the event's `pubkey` field -2. Decode the npub to get the identity pubkey as hex -3. Concatenate: `nonce + delegate_pubkey_hex + identity_pubkey_hex` -4. Compute SHA256 hash of the concatenated string -5. Sign the hash using the private key corresponding to the npub identity -6. Encode the signature as hex - -**Identity Tag Verification:** -1. Decode the npub to extract the identity public key -2. Extract the delegate pubkey from the event's `pubkey` field -3. Concatenate: `nonce + delegate_pubkey_hex + identity_pubkey_hex` -4. Compute SHA256 hash of the concatenated string -5. Verify the signature using the identity public key and computed hash -6. Reject the event if verification fails - -This binding ensures that: -- The identity holder explicitly authorized this specific delegate key -- The delegate key cannot be used with a different identity -- The signature proves both identity control and delegation authorization - -### HD Key Management Protocol - -**Client Responsibilities:** - -1. **Key Pool Management:** - - Clients MUST maintain a pool of pre-derived unused keys for each purpose - - Recommended pool size: 20 keys per purpose type - - Generate new keys when pool drops below 5 unused keys - - Publish key advertisements proactively to maintain availability - -2. **Key Consumption Tracking:** - - Mark keys as "used" when they sign any event stored in the database - - Remove used keys from the available pool - - Update local key index to prevent reuse - - Coordinate key usage across multiple client instances - -3. **Key Advertisement Publishing:** - - Publish new key advertisements when unused pool drops below threshold - - Include next sequential key indices in derivation paths - - Batch publish multiple keys to reduce network overhead - - Respect relay rate limits when publishing advertisements - -4. **Cross-Client Synchronization:** - - Clients sharing the same seed MUST coordinate key usage - - Use highest observed key index + 1 for new key generation - - Query existing key advertisements to determine current state - - Implement gap detection to identify missing key indices - -**Key Discovery Process:** -``` -1. Client starts up with shared seed -2. Query relays for existing key advertisements (Kind 39103) -3. Parse derivation paths to find highest used indices per purpose -4. Generate key pool starting from next available indices -5. Publish new key advertisements for unused keys -6. Monitor for key consumption and replenish pool as needed -``` - -**Key State Synchronization:** -- Clients SHOULD query for key advertisements on startup -- Parse `key_index` tags to determine the current key space state -- Generate keys starting from `max_observed_index + 1` -- Handle gaps in key indices gracefully (may indicate key expiration) - -### Identity Tag Usage - -The `I` tag serves multiple purposes in the consortium protocol: - -**Identity Lookup:** -- Clients can search for events using npub identities instead of raw pubkeys -- Provides a more user-friendly way to reference identities -- Enables identity-based filtering and discovery - -**Spam Prevention:** -- The proof-of-control signature prevents unauthorized use of identities -- Only the holder of the private key can create valid `I` tags -- Reduces spam by requiring cryptographic proof for each identity reference - -**Consortium Benefits:** -- Relays can index events by npub identity for efficient lookup -- Enables cross-relay identity resolution within the consortium -- Supports identity-based replication policies - -### Directory Event Types - -The following existing event kinds are considered "directory events" and subject to consortium replication: - -- **Kind 0**: User Metadata -- **Kind 3**: Follow Lists -- **Kind 5**: Event Deletion Requests -- **Kind 1984**: Reporting -- **Kind 10002**: Relay List Metadata -- **Kind 10000**: Mute Lists -- **Kind 10050**: DM Relay Lists - -### Replication Protocol - -#### 1. Consortium Formation - -1. Relay operators publish Relay Identity Announcements (Kind 39100) -2. Operators create Trust Acts (Kind 39101) toward relays they wish to collaborate with -3. When mutual trust acts exist, relays begin sharing directory events -4. Trust relationships can be inherited through the web of trust with appropriate confidence scoring - -#### 2. Directory Event Synchronization - -When a relay receives an event from a user, it: - -1. Validates the event signature and content -2. If the event contains an `I` tag, verifies the identity proof-of-control signature -3. Stores the event locally -4. Updates key delegation usage tracking (if applicable) -5. Identifies trusted consortium members based on current trust acts -6. Determines replication targets based on event kind: - - **Directory Events**: Replicate to all trusted consortium members - - **Custom Kinds**: Replicate only to relays that have specified this kind in their `K` tag -7. Replicates the event to appropriate trusted relays using Directory Event Replication Requests (Kind 39104) - -**Event Kind Matching:** -- Check each trust act's `K` tag for the event's kind number -- Only replicate to relays that have explicitly included the kind in their act -- Directory events are always replicated regardless of `K` tag contents -- Respect trust level when determining replication scope and frequency - -#### 3. Key Delegation Management - -Each relay in the consortium MUST implement key delegation limits and expiration: - -**Delegation Limits:** -- Maximum 512 unused key delegations per relay identity -- When limit is reached, oldest unused delegations are removed first -- Delegations become "used" when referenced in any stored directory event - -**Expiration Policy:** -- Key delegations expire after 30 days without usage -- Expired delegations MUST be deleted during periodic cleanup -- Usage timestamp is updated whenever a delegation is referenced - -**Cleanup Process:** -- Run cleanup at least daily to remove expired delegations -- Log delegation removals for audit purposes -- Notify consortium members of delegation changes if configured - -#### 4. Conflict Resolution - -When conflicting directory events are received (same pubkey, same kind, different content): - -1. **Timestamp Priority**: Newer events replace older ones -2. **Signature Chain Validation**: Verify the complete signature chain -3. **Identity Verification**: Validate `I` tag signatures if present -4. **Consensus Voting**: For disputed events, trusted relays vote on validity -5. **Byzantine Fault Tolerance**: System remains functional with up to 1/3 malicious nodes - -#### 5. Trust Propagation - -Trust relationships can be inherited through the web of trust: - -1. If Relay A trusts Relay B with "high" trust -2. And Relay B trusts Relay C with "medium" trust -3. Then Relay A may automatically trust Relay C with "low" trust -4. Trust inheritance follows configurable policies and confidence thresholds - -### Message Flow - -``` -Relay A Relay B Relay C - | | | - |-- Trust Act ---->| | - |<-- Trust Act ----| | - | |-- Trust Act ---->| - | |<-- Trust Act ----| - | | | - |-- Directory Event ------>|-- Directory Event ------>| - | | | - |<-- Replication Req ------|<-- Replication Req ------| - |-- Replication Resp ----->|-- Replication Resp ----->| -``` - -### Security Considerations - -1. **Identity Verification**: Relay identity keys MUST be verified through the extended NIP-11 relay information document. The relay MUST prove control of the private key through the `nonce` and `sig` fields, and the same keypair MUST be used to sign consortium events, creating a cryptographic binding between network address and relay identity. - -2. **Trust Boundaries**: Operators should carefully configure trust levels and inheritance policies - -3. **Rate Limiting**: Implement rate limiting to prevent spam and DoS attacks - -4. **Signature Validation**: All events and acts MUST be cryptographically verified, including `I` tag proof-of-control signatures - -5. **Privacy**: Sensitive consortium communications SHOULD use secp256k1 ECDH encryption - -6. **Address Binding**: The extended NIP-11 document serves as the authoritative source for relay identity verification. The signature includes the relay's network address, creating a cryptographic binding between identity, private key control, and network location. Relays MUST NOT accept consortium events from identities that cannot be verified through their NIP-11 document's signature mechanism. The `nonce` field SHOULD be regenerated periodically to maintain security. - -7. **Key Rotation**: If a relay rotates its identity key, it MUST update both its NIP-11 document and republish its Relay Identity Announcement to maintain consortium membership. - -8. **Delegation Limits**: The 512 unused key delegation limit prevents resource exhaustion attacks. Relays MUST enforce this limit strictly and implement proper cleanup mechanisms. - -9. **Identity Tag Security**: `I` tag signatures MUST be verified before accepting events. Invalid signatures MUST result in event rejection to prevent identity spoofing. The signature binds the identity to the specific delegate pubkey, preventing key reuse across different identities. - -10. **Expiration Enforcement**: The 30-day expiration policy for unused delegations MUST be enforced to prevent unbounded storage growth and maintain system performance. - -11. **Nonce Uniqueness**: Nonces in `I` tags SHOULD be unique per event to prevent replay attacks, though this is not strictly required due to the event-specific context. - -12. **Delegate Authorization**: The `I` tag signature cryptographically proves that the identity holder explicitly authorized the delegate key for this specific use. This prevents unauthorized delegation and ensures accountability for delegated actions. - -13. **HD Seed Security**: The master seed MUST be protected with the highest security measures. Compromise of the seed compromises all derived keys. Clients SHOULD use hardware security modules or secure enclaves when available. - -14. **Key Index Coordination**: Clients sharing the same seed MUST coordinate key usage to prevent index collisions. Simultaneous key generation by multiple clients could lead to the same key being used by different clients. - -15. **Side-Channel Security**: Seed sharing between clients MUST use secure, authenticated channels. Consider using encrypted messaging, secure key exchange protocols, or physical transfer for initial seed distribution. - -16. **Derivation Path Validation**: Clients MUST validate derivation paths in key advertisements to ensure they follow the specified format and prevent malicious path injection. - -### Implementation Guidelines - -#### Relay Operators - -1. Generate and securely store relay identity keys -2. Configure trust policies and act criteria -3. Implement Byzantine fault tolerance mechanisms -4. Monitor consortium health and trust relationships -5. Provide configuration options for users to opt-out of replication -6. Implement key delegation tracking and cleanup mechanisms -7. Enforce the 512 unused delegation limit per identity -8. Run daily cleanup processes to remove expired delegations -9. Validate `I` tag signatures on all incoming events -10. Maintain usage statistics for delegation management - -#### Client Developers - -1. Clients MAY display consortium membership information -2. Clients SHOULD respect user preferences for directory event replication -3. Clients MAY use consortium information for relay discovery -4. Clients SHOULD validate directory events from multiple sources -5. Clients MAY generate `I` tags with proof-of-control for identity references -6. Clients SHOULD validate `I` tag signatures when processing events -7. Clients MAY use npub identities from `I` tags for user-friendly display -8. Clients SHOULD implement proper nonce generation for `I` tag security -9. Clients MUST implement BIP32 HD key derivation for deterministic key generation -10. Clients SHOULD maintain key pools and coordinate usage across instances -11. Clients MUST query existing key advertisements on startup for synchronization -12. Clients SHOULD implement secure seed storage and sharing mechanisms -13. Clients MUST validate derivation paths in received key advertisements -14. Clients SHOULD implement key consumption tracking and pool replenishment - -### Backwards Compatibility - -This NIP is fully backwards compatible with existing Nostr implementations: - -- Relays not implementing this NIP continue to operate normally -- Directory events maintain their standard format and semantics -- Users can opt-out of consortium replication -- Existing event kinds and message types are unchanged - -## Rationale - -This design draws inspiration from the democratic Byzantine Fault Tolerant approach used in [pnyxdb](https://github.com/technicolor-research/pnyxdb), adapting it for the decentralized nature of Nostr. Key design decisions: - -1. **Separate Identity Keys**: Relay identity keys are separate from user keys to maintain clear boundaries -2. **Graduated Trust Levels**: Multiple trust levels allow for flexible consortium policies -3. **Automatic Synchronization**: Reduces user burden while maintaining decentralization -4. **Byzantine Fault Tolerance**: Ensures system reliability even with malicious participants -5. **Optional Participation**: Maintains Nostr's principle of optional protocol extensions -6. **Event Kind Specialization**: The `K` tag enables relay specialization for specific data types - -**Specialization Examples:** -- **Content Relays**: Specialize in long-form content (kind 30023), articles, and media -- **Social Relays**: Focus on social interactions (kinds 1, 6, 7) and reactions -- **Marketplace Relays**: Handle commerce events (kinds 30017, 30018) and transactions -- **Developer Relays**: Sync code-related events (kinds 1617, 1618, 1621) and repositories -- **Community Relays**: Manage community events (kinds 34550, 9000-9030) and moderation - -## Reference Implementation - -A reference implementation will be provided showing: - -1. Relay identity key generation and management -2. Trust act creation and validation -3. Directory event replication logic -4. Byzantine fault tolerance mechanisms -5. Web of trust computation algorithms -6. BIP32 HD key derivation implementation -7. Key pool management and synchronization -8. Cross-client coordination mechanisms - -### Example Key Management Workflow - -**Initial Setup:** -``` -1. Generate BIP39 mnemonic seed: "abandon abandon ... art" -2. Derive master key: m/39103'/1237'/0' -3. Share seed securely with other client instances -``` - -**Client Startup:** -``` -1. Query relays for existing key advertisements: - REQ ["sub1", {"kinds": [39103], "authors": [""]}] - -2. Parse responses to find highest key indices: - - Signing keys: max index = 15 - - Encryption keys: max index = 8 - - Delegation keys: max index = 3 - -3. Generate new key pools starting from next indices: - - m/39103'/1237'/0'/0/16 through m/39103'/1237'/0'/0/35 (signing) - - m/39103'/1237'/0'/1/9 through m/39103'/1237'/0'/1/28 (encryption) - - m/39103'/1237'/0'/2/4 through m/39103'/1237'/0'/2/23 (delegation) - -4. Publish key advertisements for new unused keys -``` - -**Key Consumption:** -``` -1. Client needs to sign an event -2. Select next unused signing key from pool -3. Sign event with selected key -4. Mark key as "used" and remove from available pool -5. If pool drops below threshold, generate and publish new keys -``` - -**Cross-Client Coordination:** -``` -1. Client A uses signing key at index 16 -2. Client B queries and sees key 16 is now used -3. Client B updates its local state and uses key 17 for next event -4. Both clients coordinate through shared relay state -``` - -## Test Vectors - -[Test vectors will be provided in a future revision] - -## Changelog - -- 2025-01-XX: Initial draft - -## Copyright - -This document is placed in the public domain. +See [NIP-XX-Cluster-Replication.md](NIP-XX-Cluster-Replication.md) for the complete specification of the new protocol. diff --git a/pkg/sync/cluster.go b/pkg/sync/cluster.go new file mode 100644 index 0000000..9fb4ddc --- /dev/null +++ b/pkg/sync/cluster.go @@ -0,0 +1,519 @@ +package sync + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "github.com/dgraph-io/badger/v4" + "lol.mleku.dev/log" + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/database/indexes/types" + "next.orly.dev/pkg/encoders/event" +) + +type ClusterManager struct { + ctx context.Context + cancel context.CancelFunc + db *database.D + adminNpubs []string + members map[string]*ClusterMember // keyed by relay URL + membersMux sync.RWMutex + pollTicker *time.Ticker + pollDone chan struct{} + httpClient *http.Client +} + +type ClusterMember struct { + HTTPURL string + WebSocketURL string + LastSerial uint64 + LastPoll time.Time + Status string // "active", "error", "unknown" + ErrorCount int +} + +type LatestSerialResponse struct { + Serial uint64 `json:"serial"` + Timestamp int64 `json:"timestamp"` +} + +type EventsRangeResponse struct { + Events []EventInfo `json:"events"` + HasMore bool `json:"has_more"` + NextFrom uint64 `json:"next_from,omitempty"` +} + +type EventInfo struct { + Serial uint64 `json:"serial"` + ID string `json:"id"` + Timestamp int64 `json:"timestamp"` +} + +func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string) *ClusterManager { + ctx, cancel := context.WithCancel(ctx) + + cm := &ClusterManager{ + ctx: ctx, + cancel: cancel, + db: db, + adminNpubs: adminNpubs, + members: make(map[string]*ClusterMember), + pollDone: make(chan struct{}), + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + + return cm +} + +func (cm *ClusterManager) Start() { + log.I.Ln("starting cluster replication manager") + + // Load persisted peer state from database + if err := cm.loadPeerState(); err != nil { + log.W.F("failed to load cluster peer state: %v", err) + } + + cm.pollTicker = time.NewTicker(5 * time.Second) + go cm.pollingLoop() +} + +func (cm *ClusterManager) Stop() { + log.I.Ln("stopping cluster replication manager") + cm.cancel() + if cm.pollTicker != nil { + cm.pollTicker.Stop() + } + <-cm.pollDone +} + +func (cm *ClusterManager) pollingLoop() { + defer close(cm.pollDone) + + for { + select { + case <-cm.ctx.Done(): + return + case <-cm.pollTicker.C: + cm.pollAllMembers() + } + } +} + +func (cm *ClusterManager) pollAllMembers() { + cm.membersMux.RLock() + members := make([]*ClusterMember, 0, len(cm.members)) + for _, member := range cm.members { + members = append(members, member) + } + cm.membersMux.RUnlock() + + for _, member := range members { + go cm.pollMember(member) + } +} + +func (cm *ClusterManager) pollMember(member *ClusterMember) { + // Get latest serial from peer + latestResp, err := cm.getLatestSerial(member.HTTPURL) + if err != nil { + log.W.F("failed to get latest serial from %s: %v", member.HTTPURL, err) + cm.updateMemberStatus(member, "error") + return + } + + cm.updateMemberStatus(member, "active") + member.LastPoll = time.Now() + + // Check if we need to fetch new events + if latestResp.Serial <= member.LastSerial { + return // No new events + } + + // Fetch events in range + from := member.LastSerial + 1 + to := latestResp.Serial + + eventsResp, err := cm.getEventsInRange(member.HTTPURL, from, to, 1000) + if err != nil { + log.W.F("failed to get events from %s: %v", member.HTTPURL, err) + return + } + + // Process fetched events + for _, eventInfo := range eventsResp.Events { + if cm.shouldFetchEvent(eventInfo) { + // Fetch full event via WebSocket and store it + if err := cm.fetchAndStoreEvent(member.WebSocketURL, eventInfo.ID); err != nil { + log.W.F("failed to fetch/store event %s from %s: %v", eventInfo.ID, member.HTTPURL, err) + } else { + log.D.F("successfully replicated event %s from %s", eventInfo.ID, member.HTTPURL) + } + } + } + + // Update last serial if we processed all events + if !eventsResp.HasMore && member.LastSerial != to { + member.LastSerial = to + // Persist the updated serial to database + if err := cm.savePeerState(member.HTTPURL, to); err != nil { + log.W.F("failed to persist serial %d for peer %s: %v", to, member.HTTPURL, err) + } + } +} + +func (cm *ClusterManager) getLatestSerial(peerURL string) (*LatestSerialResponse, error) { + url := fmt.Sprintf("%s/cluster/latest", peerURL) + resp, err := cm.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result LatestSerialResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (cm *ClusterManager) getEventsInRange(peerURL string, from, to uint64, limit int) (*EventsRangeResponse, error) { + url := fmt.Sprintf("%s/cluster/events?from=%d&to=%d&limit=%d", peerURL, from, to, limit) + resp, err := cm.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result EventsRangeResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (cm *ClusterManager) shouldFetchEvent(eventInfo EventInfo) bool { + // Relays MAY choose not to store every event they receive + // For now, accept all events + return true +} + +func (cm *ClusterManager) updateMemberStatus(member *ClusterMember, status string) { + member.Status = status + if status == "error" { + member.ErrorCount++ + } else { + member.ErrorCount = 0 + } +} + +func (cm *ClusterManager) UpdateMembership(relayURLs []string) { + cm.membersMux.Lock() + defer cm.membersMux.Unlock() + + // Remove members not in the new list + for url := range cm.members { + found := false + for _, newURL := range relayURLs { + if newURL == url { + found = true + break + } + } + if !found { + delete(cm.members, url) + // Remove persisted state for removed peer + if err := cm.removePeerState(url); err != nil { + log.W.F("failed to remove persisted state for peer %s: %v", url, err) + } + log.I.F("removed cluster member: %s", url) + } + } + + // Add new members + for _, url := range relayURLs { + if _, exists := cm.members[url]; !exists { + // For simplicity, assume HTTP and WebSocket URLs are the same + // In practice, you'd need to parse these properly + member := &ClusterMember{ + HTTPURL: url, + WebSocketURL: url, // TODO: Convert to WebSocket URL + LastSerial: 0, + Status: "unknown", + } + cm.members[url] = member + log.I.F("added cluster member: %s", url) + } + } +} + +// HandleMembershipEvent processes a cluster membership event (Kind 39108) +func (cm *ClusterManager) HandleMembershipEvent(event *event.E) error { + // Verify the event is signed by a cluster admin + adminFound := false + for _, adminNpub := range cm.adminNpubs { + // TODO: Convert adminNpub to pubkey and verify signature + // For now, accept all events (this should be properly validated) + _ = adminNpub // Mark as used to avoid compiler warning + adminFound = true + break + } + + if !adminFound { + return fmt.Errorf("event not signed by cluster admin") + } + + // Parse the relay URLs from the tags + var relayURLs []string + for _, tag := range *event.Tags { + if len(tag.T) >= 2 && string(tag.T[0]) == "relay" { + relayURLs = append(relayURLs, string(tag.T[1])) + } + } + + if len(relayURLs) == 0 { + return fmt.Errorf("no relay URLs found in membership event") + } + + // Update cluster membership + cm.UpdateMembership(relayURLs) + + log.I.F("updated cluster membership with %d relays from event %x", len(relayURLs), event.ID) + + return nil +} + +// HTTP Handlers + +func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Get the latest serial from database by querying for the highest serial + latestSerial, err := cm.getLatestSerialFromDB() + if err != nil { + log.W.F("failed to get latest serial: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + response := LatestSerialResponse{ + Serial: latestSerial, + Timestamp: time.Now().Unix(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse query parameters + fromStr := r.URL.Query().Get("from") + toStr := r.URL.Query().Get("to") + limitStr := r.URL.Query().Get("limit") + + from := uint64(0) + to := uint64(0) + limit := 1000 + + if fromStr != "" { + fmt.Sscanf(fromStr, "%d", &from) + } + if toStr != "" { + fmt.Sscanf(toStr, "%d", &to) + } + if limitStr != "" { + fmt.Sscanf(limitStr, "%d", &limit) + if limit > 10000 { + limit = 10000 + } + } + + // Get events in range + events, hasMore, nextFrom, err := cm.getEventsInRangeFromDB(from, to, int(limit)) + if err != nil { + log.W.F("failed to get events in range: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + response := EventsRangeResponse{ + Events: events, + HasMore: hasMore, + NextFrom: nextFrom, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (cm *ClusterManager) getLatestSerialFromDB() (uint64, error) { + // Query the database to find the highest serial number + // We'll iterate through the event keys to find the maximum serial + var maxSerial uint64 = 0 + + err := cm.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.IteratorOptions{ + Reverse: true, // Start from highest + Prefix: []byte{0}, // Event keys start with 0 + }) + defer it.Close() + + // Look for the first event key (which should have the highest serial in reverse iteration) + it.Seek([]byte{0}) + if it.Valid() { + key := it.Item().Key() + if len(key) >= 5 { // Serial is in the last 5 bytes + serial := binary.BigEndian.Uint64(key[len(key)-8:]) >> 24 // Convert from Uint40 + if serial > maxSerial { + maxSerial = serial + } + } + } + + return nil + }) + + return maxSerial, err +} + +func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([]EventInfo, bool, uint64, error) { + var events []EventInfo + var hasMore bool + var nextFrom uint64 + + // Convert serials to Uint40 format for querying + fromSerial := &types.Uint40{} + toSerial := &types.Uint40{} + + if err := fromSerial.Set(from); err != nil { + return nil, false, 0, err + } + if err := toSerial.Set(to); err != nil { + return nil, false, 0, err + } + + // Query events by serial range + // This is a simplified implementation - in practice you'd need to use the proper indexing + err := cm.db.View(func(txn *badger.Txn) error { + // For now, return empty results as this requires more complex indexing logic + // TODO: Implement proper serial range querying using database indexes + return nil + }) + + return events, hasMore, nextFrom, err +} + +func (cm *ClusterManager) fetchAndStoreEvent(wsURL, eventID string) error { + // TODO: Implement WebSocket connection and event fetching + // For now, this is a placeholder that assumes the event can be fetched + // In a full implementation, this would: + // 1. Connect to the WebSocket endpoint + // 2. Send a REQ message for the specific event ID + // 3. Receive the EVENT message + // 4. Validate and store the event in the local database + + // Placeholder - mark as not implemented for now + log.D.F("fetchAndStoreEvent called for %s from %s (placeholder implementation)", eventID, wsURL) + return nil // Return success for now +} + +// Database key prefixes for cluster state persistence +const ( + clusterPeerStatePrefix = "cluster:peer:" +) + +// loadPeerState loads persisted peer state from the database +func (cm *ClusterManager) loadPeerState() error { + cm.membersMux.Lock() + defer cm.membersMux.Unlock() + + prefix := []byte(clusterPeerStatePrefix) + return cm.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.IteratorOptions{ + Prefix: prefix, + }) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + key := item.Key() + + // Extract peer URL from key (remove prefix) + peerURL := string(key[len(prefix):]) + + // Read the serial value + var serial uint64 + err := item.Value(func(val []byte) error { + if len(val) == 8 { + serial = binary.BigEndian.Uint64(val) + } + return nil + }) + if err != nil { + log.W.F("failed to read peer state for %s: %v", peerURL, err) + continue + } + + // Update existing member or create new one + if member, exists := cm.members[peerURL]; exists { + member.LastSerial = serial + log.D.F("loaded persisted serial %d for existing peer %s", serial, peerURL) + } else { + // Create member with persisted state + member := &ClusterMember{ + HTTPURL: peerURL, + WebSocketURL: peerURL, // TODO: Convert to WebSocket URL + LastSerial: serial, + Status: "unknown", + } + cm.members[peerURL] = member + log.D.F("loaded persisted serial %d for new peer %s", serial, peerURL) + } + } + return nil + }) +} + +// savePeerState saves the current serial for a peer to the database +func (cm *ClusterManager) savePeerState(peerURL string, serial uint64) error { + key := []byte(clusterPeerStatePrefix + peerURL) + value := make([]byte, 8) + binary.BigEndian.PutUint64(value, serial) + + return cm.db.Update(func(txn *badger.Txn) error { + return txn.Set(key, value) + }) +} + +// removePeerState removes persisted state for a peer from the database +func (cm *ClusterManager) removePeerState(peerURL string) error { + key := []byte(clusterPeerStatePrefix + peerURL) + + return cm.db.Update(func(txn *badger.Txn) error { + return txn.Delete(key) + }) +}