Refactor NIP-XX Document and Protocol Implementation for Directory Consensus

- Updated the NIP-XX document to clarify terminology, replacing "attestations" with "acts" for consistency.
- Enhanced the protocol by introducing new event kinds: Trust Act (Kind 39101) and Group Tag Act (Kind 39102), with detailed specifications for their structure and usage.
- Modified the signature generation process to include the canonical WebSocket URL, ensuring proper binding and verification.
- Improved validation mechanisms for identity tags and event replication requests, reinforcing security and integrity within the directory consensus protocol.
- Added comprehensive documentation for new event types and their respective validation processes, ensuring clarity for developers and users.
- Introduced new helper functions and structures to facilitate the creation and management of directory events and acts.
This commit is contained in:
2025-10-25 12:33:47 +01:00
parent f0e89c84bd
commit 5652cec845
14 changed files with 3287 additions and 44 deletions

View File

@@ -166,7 +166,7 @@ func (s *Server) WebSocketURL(req *http.Request) (url string) {
if host == "" { if host == "" {
host = req.Host host = req.Host
} }
return proto + "://" + host return proto + "://" + strings.TrimRight(host, "/") + "/"
} }
func (s *Server) DashboardURL(req *http.Request) (url string) { func (s *Server) DashboardURL(req *http.Request) (url string) {

View File

@@ -28,7 +28,7 @@ This NIP addresses these issues by enabling relay operators to form trusted cons
Each participating relay MUST generate and maintain a long-term identity keypair separate from any user 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. - **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 attestations and directory events - **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 - **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://<relay-domain>/.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. 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://<relay-domain>/.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.
@@ -57,7 +57,7 @@ This protocol extends the NIP-11 relay information document with two additional
**Signature Generation:** **Signature Generation:**
1. Concatenate the `pubkey`, `nonce`, and relay address as strings: `pubkey + nonce + relay_address` 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") 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 3. Compute SHA256 hash of the concatenated string
4. Sign the hash using the relay's private key (corresponding to `pubkey`) 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 5. Encode the signature as hex and store in the `sig` field
@@ -79,8 +79,8 @@ This NIP defines the following new event kinds:
| Kind | Description | | Kind | Description |
|------|-------------| |------|-------------|
| `39100` | Relay Identity Announcement | | `39100` | Relay Identity Announcement |
| `39101` | Trust Attestation | | `39101` | Trust Act |
| `39102` | Group Tag Attestation | | `39102` | Group Tag Act |
| `39103` | Public Key Advertisement | | `39103` | Public Key Advertisement |
| `39104` | Directory Event Replication Request | | `39104` | Directory Event Replication Request |
| `39105` | Directory Event Replication Response | | `39105` | Directory Event Replication Response |
@@ -107,7 +107,7 @@ Relay operators publish this event to announce their participation in the distri
**Tags:** **Tags:**
- `d`: Identifier for the relay identity (always "relay-identity") - `d`: Identifier for the relay identity (always "relay-identity")
- `relay`: WebSocket URL of the relay - `relay`: WebSocket URL of the relay
- `signing_key`: Public key for verifying attestations from this relay (MAY be the same as identity key) - `signing_key`: Public key for verifying acts from this relay (MAY be the same as identity key)
- `encryption_key`: Public key for ECDH encryption - `encryption_key`: Public key for ECDH encryption
- `version`: Protocol version number - `version`: Protocol version number
- `nip11_url`: URL to the relay's NIP-11 information document for identity verification - `nip11_url`: URL to the relay's NIP-11 information document for identity verification
@@ -125,9 +125,9 @@ Relay operators publish this event to announce their participation in the distri
5. They verify that the announcement event is signed by the same key 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 6. This confirms that the relay identity is cryptographically bound to the specific network address
### Trust Attestation (Kind 39101) ### Trust Act (Kind 39101)
Relay operators create trust attestations toward other relays they wish to enter consensus with: Relay operators create trust acts toward other relays they wish to enter consensus with:
```json ```json
{ {
@@ -149,7 +149,7 @@ Relay operators create trust attestations toward other relays they wish to enter
- `p`: Public key of the target relay being attested - `p`: Public key of the target relay being attested
- `trust_level`: Level of trust (high, medium, low) - `trust_level`: Level of trust (high, medium, low)
- `relay`: WebSocket URL of the target relay - `relay`: WebSocket URL of the target relay
- `expiry`: Optional expiration timestamp for the attestation - `expiry`: Optional expiration timestamp for the act
- `reason`: How this trust relationship was established - `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) - `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) - `I`: Identity tag with npub, nonce, and proof-of-control signature (same format as Kind 39103)
@@ -166,7 +166,7 @@ Relay operators create trust attestations toward other relays they wish to enter
- **Near Real-time**: Events matching `K` tag kinds are replicated with minimal delay - **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 - **Bidirectional**: Replication occurs both to and from the trusted relay for specified kinds
### Group Tag Attestation (Kind 39102) ### Group Tag Act (Kind 39102)
Relays can attest to arbitrary string values used as tags to create logical groups: Relays can attest to arbitrary string values used as tags to create logical groups:
@@ -184,10 +184,10 @@ Relays can attest to arbitrary string values used as tags to create logical grou
``` ```
**Tags:** **Tags:**
- `d`: Unique identifier for this group attestation - `d`: Unique identifier for this group act
- `group_tag`: The tag name and value being attested - `group_tag`: The tag name and value being attested
- `attestor`: Public key of the relay making the attestation - `actor`: Public key of the relay making the act
- `confidence`: Confidence level (0-100) in this attestation - `confidence`: Confidence level (0-100) in this act
### Hierarchical Deterministic Key Derivation ### Hierarchical Deterministic Key Derivation
@@ -363,8 +363,8 @@ The following existing event kinds are considered "directory events" and subject
#### 1. Consortium Formation #### 1. Consortium Formation
1. Relay operators publish Relay Identity Announcements (Kind 39100) 1. Relay operators publish Relay Identity Announcements (Kind 39100)
2. Operators create Trust Attestations (Kind 39101) toward relays they wish to collaborate with 2. Operators create Trust Acts (Kind 39101) toward relays they wish to collaborate with
3. When mutual trust attestations exist, relays begin sharing directory events 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 4. Trust relationships can be inherited through the web of trust with appropriate confidence scoring
#### 2. Directory Event Synchronization #### 2. Directory Event Synchronization
@@ -375,15 +375,15 @@ When a relay receives an event from a user, it:
2. If the event contains an `I` tag, verifies the identity proof-of-control signature 2. If the event contains an `I` tag, verifies the identity proof-of-control signature
3. Stores the event locally 3. Stores the event locally
4. Updates key delegation usage tracking (if applicable) 4. Updates key delegation usage tracking (if applicable)
5. Identifies trusted consortium members based on current trust attestations 5. Identifies trusted consortium members based on current trust acts
6. Determines replication targets based on event kind: 6. Determines replication targets based on event kind:
- **Directory Events**: Replicate to all trusted consortium members - **Directory Events**: Replicate to all trusted consortium members
- **Custom Kinds**: Replicate only to relays that have specified this kind in their `K` tag - **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) 7. Replicates the event to appropriate trusted relays using Directory Event Replication Requests (Kind 39104)
**Event Kind Matching:** **Event Kind Matching:**
- Check each trust attestation's `K` tag for the event's kind number - 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 attestation - Only replicate to relays that have explicitly included the kind in their act
- Directory events are always replicated regardless of `K` tag contents - Directory events are always replicated regardless of `K` tag contents
- Respect trust level when determining replication scope and frequency - Respect trust level when determining replication scope and frequency
@@ -430,10 +430,10 @@ Trust relationships can be inherited through the web of trust:
``` ```
Relay A Relay B Relay C Relay A Relay B Relay C
| | | | | |
|-- Trust Attestation ---->| | |-- Trust Act ---->| |
|<-- Trust Attestation ----| | |<-- Trust Act ----| |
| |-- Trust Attestation ---->| | |-- Trust Act ---->|
| |<-- Trust Attestation ----| | |<-- Trust Act ----|
| | | | | |
|-- Directory Event ------>|-- Directory Event ------>| |-- Directory Event ------>|-- Directory Event ------>|
| | | | | |
@@ -449,7 +449,7 @@ Relay A Relay B Relay C
3. **Rate Limiting**: Implement rate limiting to prevent spam and DoS attacks 3. **Rate Limiting**: Implement rate limiting to prevent spam and DoS attacks
4. **Signature Validation**: All events and attestations MUST be cryptographically verified, including `I` tag proof-of-control signatures 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 5. **Privacy**: Sensitive consortium communications SHOULD use secp256k1 ECDH encryption
@@ -480,7 +480,7 @@ Relay A Relay B Relay C
#### Relay Operators #### Relay Operators
1. Generate and securely store relay identity keys 1. Generate and securely store relay identity keys
2. Configure trust policies and attestation criteria 2. Configure trust policies and act criteria
3. Implement Byzantine fault tolerance mechanisms 3. Implement Byzantine fault tolerance mechanisms
4. Monitor consortium health and trust relationships 4. Monitor consortium health and trust relationships
5. Provide configuration options for users to opt-out of replication 5. Provide configuration options for users to opt-out of replication
@@ -539,7 +539,7 @@ This design draws inspiration from the democratic Byzantine Fault Tolerant appro
A reference implementation will be provided showing: A reference implementation will be provided showing:
1. Relay identity key generation and management 1. Relay identity key generation and management
2. Trust attestation creation and validation 2. Trust act creation and validation
3. Directory event replication logic 3. Directory event replication logic
4. Byzantine fault tolerance mechanisms 4. Byzantine fault tolerance mechanisms
5. Web of trust computation algorithms 5. Web of trust computation algorithms

2
go.mod
View File

@@ -27,7 +27,6 @@ require (
require ( require (
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/fgprof v0.9.5 // indirect
@@ -35,7 +34,6 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/cpu v0.1.1 // indirect

17
go.sum
View File

@@ -10,7 +10,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
@@ -44,19 +43,13 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20251002213607-436353cc1ee6 h1:/WHh/1k4thM/w+PAZEIiZK9NwCMFahw5tUzKUCnUtds=
github.com/google/pprof v0.0.0-20251002213607-436353cc1ee6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA=
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -103,25 +96,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/exp/typeparams v0.0.0-20251002181428-27f1f14c8bb9 h1:EvjuVHWMoRaAxH402KMgrQpGUjoBy/OWvZjLOqQnwNk=
golang.org/x/exp/typeparams v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE= golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE=
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA= golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA=
golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -138,8 +123,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=

View File

@@ -0,0 +1,376 @@
// Package directory implements the distributed directory consensus protocol
// as defined in NIP-XX for Nostr relay operators.
//
// # Overview
//
// This package provides complete message encoding, validation, and helper
// functions for implementing the distributed directory consensus protocol.
// The protocol enables Nostr relay operators to form trusted consortiums
// that automatically synchronize essential identity-related events while
// maintaining decentralization and Byzantine fault tolerance.
//
// # Event Kinds
//
// The protocol defines six new event kinds:
//
// - 39100: Relay Identity Announcement - Announces relay participation
// - 39101: Trust Act - Creates trust relationships between relays
// - 39102: Group Tag Act - Attests to arbitrary string values
// - 39103: Public Key Advertisement - Advertises HD-derived keys
// - 39104: Directory Event Replication Request - Requests event replication
// - 39105: Directory Event Replication Response - Responds to replication requests
//
// # Directory Events
//
// The following existing event kinds are considered "directory events" and
// are automatically replicated among consortium members:
//
// - 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
//
// # Basic Usage
//
// ## Creating a Relay Identity Announcement
//
// pubkey := []byte{...} // 32-byte relay identity key
// announcement, err := directory.NewRelayIdentityAnnouncement(
// pubkey,
// "relay.example.com", // name
// "A community relay", // description
// "admin@example.com", // contact
// "wss://relay.example.com", // relay URL
// "abc123...", // signing key (hex)
// "def456...", // encryption key (hex)
// "1", // version
// "https://relay.example.com/.well-known/nostr.json", // NIP-11 URL
// )
// if err != nil {
// log.Fatal(err)
// }
//
// ## Creating a Trust Act
//
// act, err := directory.NewTrustAct(
// pubkey,
// "target_relay_pubkey_hex", // target relay
// directory.TrustLevelHigh, // trust level
// "wss://target.relay.com", // target URL
// nil, // no expiry
// directory.TrustReasonManual, // manual trust
// []uint16{1, 6, 7}, // additional kinds to replicate
// nil, // no identity tag
// )
//
// ## Creating a Public Key Advertisement
//
// validFrom := time.Now()
// validUntil := validFrom.Add(30 * 24 * time.Hour) // 30 days
//
// keyAd, err := directory.NewPublicKeyAdvertisement(
// pubkey,
// "signing-key-001", // key ID
// "fedcba9876543210...", // public key (hex)
// directory.KeyPurposeSigning, // purpose
// validFrom, // valid from
// validUntil, // valid until
// "secp256k1", // algorithm
// "m/39103'/1237'/0'/0/1", // derivation path
// 1, // key index
// nil, // no identity tag
// )
//
// # Identity Tags
//
// Identity tags (I tags) provide npub-encoded identities with proof-of-control
// signatures. They bind an identity to a specific delegate key, preventing
// unauthorized use.
//
// ## Creating Identity Tags
//
// // Create identity tag builder with private key
// identityPrivkey := []byte{...} // 32-byte private key
// builder, err := directory.NewIdentityTagBuilder(identityPrivkey)
// if err != nil {
// log.Fatal(err)
// }
//
// // Create signed identity tag for delegate key
// delegatePubkey := []byte{...} // 32-byte delegate public key
// identityTag, err := builder.CreateIdentityTag(delegatePubkey)
// if err != nil {
// log.Fatal(err)
// }
//
// // Use in trust act
// act, err := directory.NewTrustAct(
// pubkey,
// "target_relay_pubkey_hex",
// directory.TrustLevelHigh,
// "wss://target.relay.com",
// nil,
// directory.TrustReasonManual,
// []uint16{1, 6, 7},
// identityTag, // Include identity tag
// )
//
// # Validation
//
// All message types include comprehensive validation:
//
// // Validate a parsed event
// if err := announcement.Validate(); err != nil {
// log.Printf("Invalid announcement: %v", err)
// return
// }
//
// // Validate any consortium event
// if err := directory.ValidateConsortiumEvent(event); err != nil {
// log.Printf("Invalid consortium event: %v", err)
// return
// }
//
// // Verify NIP-11 binding
// valid, err := directory.ValidateRelayIdentityBinding(
// announcement,
// nip11Pubkey,
// nip11Nonce,
// nip11Sig,
// relayAddress,
// )
// if err != nil || !valid {
// log.Printf("Invalid relay identity binding")
// return
// }
//
// # Trust Calculation
//
// The package provides utilities for calculating trust relationships:
//
// // Create trust calculator
// calculator := directory.NewTrustCalculator()
//
// // Add trust acts
// calculator.AddAct(act1)
// calculator.AddAct(act2)
//
// // Get direct trust level
// level := calculator.GetTrustLevel("relay_pubkey_hex")
//
// // Calculate inherited trust
// inheritedLevel := calculator.CalculateInheritedTrust(
// "from_relay_pubkey",
// "to_relay_pubkey",
// )
//
// # Replication Filtering
//
// Determine which events should be replicated to which relays:
//
// // Create replication filter
// filter := directory.NewReplicationFilter(calculator)
// filter.AddTrustAct(act)
//
// // Check if event should be replicated
// shouldReplicate := filter.ShouldReplicate(event, "target_relay_pubkey")
//
// // Get all replication targets for an event
// targets := filter.GetReplicationTargets(event)
//
// # Event Batching
//
// Batch events for efficient replication:
//
// // Create event batcher
// batcher := directory.NewEventBatcher(100) // max 100 events per batch
//
// // Add events to batches
// batcher.AddEvent("wss://relay1.com", event1)
// batcher.AddEvent("wss://relay1.com", event2)
// batcher.AddEvent("wss://relay2.com", event3)
//
// // Check if batch is full
// if batcher.IsBatchFull("wss://relay1.com") {
// batch := batcher.FlushBatch("wss://relay1.com")
// // Send batch for replication
// }
//
// # Replication Requests and Responses
//
// ## Creating Replication Requests
//
// requestID, err := directory.GenerateRequestID()
// if err != nil {
// log.Fatal(err)
// }
//
// request, err := directory.NewDirectoryEventReplicationRequest(
// pubkey,
// requestID,
// "wss://target.relay.com",
// []*event.E{event1, event2, event3},
// )
//
// ## Creating Replication Responses
//
// // Success response
// results := []*directory.EventResult{
// directory.CreateEventResult("event_id_1", true, ""),
// directory.CreateEventResult("event_id_2", false, "duplicate event"),
// }
//
// response, err := directory.CreateSuccessResponse(
// pubkey,
// requestID,
// "wss://source.relay.com",
// results,
// )
//
// // Error response
// errorResponse, err := directory.CreateErrorResponse(
// pubkey,
// requestID,
// "wss://source.relay.com",
// "relay temporarily unavailable",
// )
//
// # Key Management
//
// The protocol uses BIP32 HD key derivation for deterministic key generation:
//
// // Create key pool manager
// masterSeed := []byte{...} // BIP39 seed
// manager := directory.NewKeyPoolManager(masterSeed, 0) // identity index 0
//
// // Generate derivation paths
// signingPath := manager.GenerateDerivationPath(directory.KeyPurposeSigning, 5)
// // Returns: "m/39103'/1237'/0'/0/5"
//
// encryptionPath := manager.GenerateDerivationPath(directory.KeyPurposeEncryption, 3)
// // Returns: "m/39103'/1237'/0'/1/3"
//
// // Track key usage
// nextIndex := manager.GetNextKeyIndex(directory.KeyPurposeSigning)
// manager.SetKeyIndex(directory.KeyPurposeSigning, 10) // Skip to index 10
//
// # Error Handling
//
// All functions return detailed errors using the errorf package:
//
// announcement, err := directory.ParseRelayIdentityAnnouncement(event)
// if err != nil {
// // Handle specific error types
// switch {
// case strings.Contains(err.Error(), "invalid event kind"):
// log.Printf("Wrong event kind: %v", err)
// case strings.Contains(err.Error(), "missing"):
// log.Printf("Missing required field: %v", err)
// default:
// log.Printf("Parse error: %v", err)
// }
// return
// }
//
// # Security Considerations
//
// The package implements several security measures:
//
// - All events must have valid signatures
// - Identity tags prevent unauthorized identity use
// - NIP-11 binding prevents relay impersonation
// - Timestamp validation prevents replay attacks
// - Content size limits prevent DoS attacks
// - Nonce validation ensures cryptographic security
//
// # Protocol Constants
//
// Important protocol constants:
//
// - MaxKeyDelegations: 512 unused key delegations per identity
// - KeyExpirationDays: 30 days for unused key delegations
// - MinNonceSize: 16 bytes minimum for nonces
// - MaxContentLength: 65536 bytes maximum for event content
//
// # Integration Example
//
// Complete example of implementing consortium membership:
//
// package main
//
// import (
// "log"
// "time"
//
// "next.orly.dev/pkg/protocol/directory"
// )
//
// func main() {
// // Generate relay identity key
// relayPrivkey := []byte{...} // 32 bytes
// relayPubkey := schnorr.PubkeyFromSeckey(relayPrivkey)
//
// // Create relay identity announcement
// announcement, err := directory.NewRelayIdentityAnnouncement(
// relayPubkey,
// "my-relay.com",
// "My Community Relay",
// "admin@my-relay.com",
// "wss://my-relay.com",
// hex.EncodeToString(signingPubkey),
// hex.EncodeToString(encryptionPubkey),
// "1",
// "https://my-relay.com/.well-known/nostr.json",
// )
// if err != nil {
// log.Fatal(err)
// }
//
// // Sign and publish announcement
// if err := announcement.Event.Sign(relayPrivkey); err != nil {
// log.Fatal(err)
// }
//
// // Create trust act for another relay
// act, err := directory.NewTrustAct(
// relayPubkey,
// "trusted_relay_pubkey_hex",
// directory.TrustLevelHigh,
// "wss://trusted-relay.com",
// nil, // no expiry
// directory.TrustReasonManual,
// []uint16{1, 6, 7}, // replicate text notes, reposts, reactions
// nil, // no identity tag
// )
// if err != nil {
// log.Fatal(err)
// }
//
// // Sign and publish act
// if err := act.Event.Sign(relayPrivkey); err != nil {
// log.Fatal(err)
// }
//
// // Set up replication filter
// calculator := directory.NewTrustCalculator()
// calculator.AddAct(act)
//
// filter := directory.NewReplicationFilter(calculator)
// filter.AddTrustAct(act)
//
// // When receiving events, check if they should be replicated
// for event := range eventChannel {
// targets := filter.GetReplicationTargets(event)
// for _, target := range targets {
// // Replicate event to target relay
// replicateEvent(event, target)
// }
// }
// }
//
// For more detailed examples and advanced usage patterns, see the test files
// and the reference implementation in the main relay codebase.
package directory

View File

@@ -0,0 +1,241 @@
package directory
import (
"strconv"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// GroupTagAct represents a complete Group Tag Act event
// (Kind 39102) with typed access to its components.
type GroupTagAct struct {
Event *event.E
GroupID string
TagName string
TagValue string
Actor string
Confidence int
Description string
}
// NewGroupTagAct creates a new Group Tag Act event.
func NewGroupTagAct(
pubkey []byte,
groupID, tagName, tagValue, actor string,
confidence int,
description string,
) (gta *GroupTagAct, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if groupID == "" {
return nil, errorf.E("group ID is required")
}
if tagName == "" {
return nil, errorf.E("tag name is required")
}
if tagValue == "" {
return nil, errorf.E("tag value is required")
}
if actor == "" {
return nil, errorf.E("actor is required")
}
if len(actor) != 64 {
return nil, errorf.E("actor must be 64 hex characters")
}
if confidence < 0 || confidence > 100 {
return nil, errorf.E("confidence must be between 0 and 100")
}
// Create base event
ev := CreateBaseEvent(pubkey, GroupTagActKind)
ev.Content = []byte(description)
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(DTag), groupID))
ev.Tags.Append(tag.NewFromAny(string(GroupTagTag), tagName, tagValue))
ev.Tags.Append(tag.NewFromAny(string(ActorTag), actor))
ev.Tags.Append(tag.NewFromAny(string(ConfidenceTag), strconv.Itoa(confidence)))
gta = &GroupTagAct{
Event: ev,
GroupID: groupID,
TagName: tagName,
TagValue: tagValue,
Actor: actor,
Confidence: confidence,
Description: description,
}
return
}
// ParseGroupTagAct parses an event into a GroupTagAct
// structure with validation.
func ParseGroupTagAct(ev *event.E) (gta *GroupTagAct, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != GroupTagActKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
GroupTagActKind.K, ev.Kind)
}
// Extract required tags
dTag := ev.Tags.GetFirst(DTag)
if dTag == nil {
return nil, errorf.E("missing d tag")
}
groupTagTag := ev.Tags.GetFirst(GroupTagTag)
if groupTagTag == nil {
return nil, errorf.E("missing group_tag tag")
}
// Validate group_tag has at least 2 elements (name and value)
if groupTagTag.Len() < 3 { // "group_tag", name, value
return nil, errorf.E("group_tag must have name and value")
}
actorTag := ev.Tags.GetFirst(ActorTag)
if actorTag == nil {
return nil, errorf.E("missing actor tag")
}
confidenceTag := ev.Tags.GetFirst(ConfidenceTag)
if confidenceTag == nil {
return nil, errorf.E("missing confidence tag")
}
// Parse confidence
var confidence int
if confidence, err = strconv.Atoi(string(confidenceTag.Value())); chk.E(err) {
return nil, errorf.E("invalid confidence value: %w", err)
}
if confidence < 0 || confidence > 100 {
return nil, errorf.E("confidence must be between 0 and 100")
}
gta = &GroupTagAct{
Event: ev,
GroupID: string(dTag.Value()),
TagName: string(groupTagTag.T[1]),
TagValue: string(groupTagTag.T[2]),
Actor: string(actorTag.Value()),
Confidence: confidence,
Description: string(ev.Content),
}
return
}
// Validate performs comprehensive validation of a GroupTagAct.
func (gta *GroupTagAct) Validate() (err error) {
if gta == nil {
return errorf.E("GroupTagAct cannot be nil")
}
if gta.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = gta.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if gta.GroupID == "" {
return errorf.E("group ID is required")
}
if gta.TagName == "" {
return errorf.E("tag name is required")
}
if gta.TagValue == "" {
return errorf.E("tag value is required")
}
if gta.Actor == "" {
return errorf.E("actor is required")
}
if len(gta.Actor) != 64 {
return errorf.E("actor must be 64 hex characters")
}
if gta.Confidence < 0 || gta.Confidence > 100 {
return errorf.E("confidence must be between 0 and 100")
}
return nil
}
// GetGroupID returns the group identifier.
func (gta *GroupTagAct) GetGroupID() string {
return gta.GroupID
}
// GetTagName returns the tag name being attested.
func (gta *GroupTagAct) GetTagName() string {
return gta.TagName
}
// GetTagValue returns the tag value being attested.
func (gta *GroupTagAct) GetTagValue() string {
return gta.TagValue
}
// GetActor returns the public key of the relay making the act.
func (gta *GroupTagAct) GetActor() string {
return gta.Actor
}
// GetConfidence returns the confidence level (0-100) in this act.
func (gta *GroupTagAct) GetConfidence() int {
return gta.Confidence
}
// GetDescription returns the optional description of the act.
func (gta *GroupTagAct) GetDescription() string {
return gta.Description
}
// IsHighConfidence returns true if the confidence level is 80 or higher.
func (gta *GroupTagAct) IsHighConfidence() bool {
return gta.Confidence >= 80
}
// IsMediumConfidence returns true if the confidence level is between 50 and 79.
func (gta *GroupTagAct) IsMediumConfidence() bool {
return gta.Confidence >= 50 && gta.Confidence < 80
}
// IsLowConfidence returns true if the confidence level is below 50.
func (gta *GroupTagAct) IsLowConfidence() bool {
return gta.Confidence < 50
}
// MatchesTag returns true if this act matches the given tag name and value.
func (gta *GroupTagAct) MatchesTag(name, value string) bool {
return gta.TagName == name && gta.TagValue == value
}
// MatchesGroup returns true if this act belongs to the given group.
func (gta *GroupTagAct) MatchesGroup(groupID string) bool {
return gta.GroupID == groupID
}
// IsAttestedBy returns true if this act was made by the given actor.
func (gta *GroupTagAct) IsAttestedBy(actor string) bool {
return gta.Actor == actor
}

View File

@@ -0,0 +1,426 @@
package directory
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/crypto/ec/schnorr"
"next.orly.dev/pkg/crypto/ec/secp256k1"
"next.orly.dev/pkg/encoders/bech32encoding"
"next.orly.dev/pkg/encoders/event"
)
// IdentityTagBuilder helps construct identity tags with proper signatures.
type IdentityTagBuilder struct {
identityPrivkey []byte
identityPubkey []byte
npubIdentity string
}
// NewIdentityTagBuilder creates a new identity tag builder with the given
// identity private key.
func NewIdentityTagBuilder(identityPrivkey []byte) (builder *IdentityTagBuilder, err error) {
if len(identityPrivkey) != 32 {
return nil, errorf.E("identity private key must be 32 bytes")
}
// Derive public key from secret key
identitySecKey := secp256k1.SecKeyFromBytes(identityPrivkey)
identityPubkey := identitySecKey.PubKey()
identityPubkeyBytes := schnorr.SerializePubKey(identityPubkey)
// Encode as npub
var npubIdentity []byte
if npubIdentity, err = bech32encoding.PublicKeyToNpub(identityPubkey); chk.E(err) {
return nil, errorf.E("failed to encode npub: %w", err)
}
return &IdentityTagBuilder{
identityPrivkey: identityPrivkey,
identityPubkey: identityPubkeyBytes,
npubIdentity: string(npubIdentity),
}, nil
}
// CreateIdentityTag creates a signed identity tag for the given delegate pubkey.
func (builder *IdentityTagBuilder) CreateIdentityTag(delegatePubkey []byte) (identityTag *IdentityTag, err error) {
if len(delegatePubkey) != 32 {
return nil, errorf.E("delegate pubkey must be 32 bytes")
}
// Generate nonce
var nonceHex string
if nonceHex, err = GenerateNonceHex(16); chk.E(err) {
return nil, errorf.E("failed to generate nonce: %w", err)
}
// Create message: nonce + delegate_pubkey_hex + identity_pubkey_hex
delegatePubkeyHex := hex.EncodeToString(delegatePubkey)
identityPubkeyHex := hex.EncodeToString(builder.identityPubkey)
message := nonceHex + delegatePubkeyHex + identityPubkeyHex
// Hash and sign
hash := sha256.Sum256([]byte(message))
identitySecKey := secp256k1.SecKeyFromBytes(builder.identityPrivkey)
var sig *schnorr.Signature
if sig, err = schnorr.Sign(identitySecKey, hash[:]); chk.E(err) {
return nil, errorf.E("failed to sign identity tag: %w", err)
}
signature := sig.Serialize()
identityTag = &IdentityTag{
NPubIdentity: builder.npubIdentity,
Nonce: nonceHex,
Signature: hex.EncodeToString(signature),
}
return
}
// GetNPubIdentity returns the npub-encoded identity.
func (builder *IdentityTagBuilder) GetNPubIdentity() string {
return builder.npubIdentity
}
// GetIdentityPubkey returns the raw identity public key.
func (builder *IdentityTagBuilder) GetIdentityPubkey() []byte {
return builder.identityPubkey
}
// KeyPoolManager helps manage HD key derivation and advertisement.
type KeyPoolManager struct {
masterSeed []byte
identityIndex uint32
currentIndices map[KeyPurpose]int
}
// NewKeyPoolManager creates a new key pool manager with the given master seed.
func NewKeyPoolManager(masterSeed []byte, identityIndex uint32) *KeyPoolManager {
return &KeyPoolManager{
masterSeed: masterSeed,
identityIndex: identityIndex,
currentIndices: make(map[KeyPurpose]int),
}
}
// GenerateDerivationPath creates a BIP32 derivation path for the given purpose and index.
func (kpm *KeyPoolManager) GenerateDerivationPath(purpose KeyPurpose, index int) string {
var usageIndex int
switch purpose {
case KeyPurposeSigning:
usageIndex = 0
case KeyPurposeEncryption:
usageIndex = 1
case KeyPurposeDelegation:
usageIndex = 2
default:
usageIndex = 0
}
return fmt.Sprintf("m/39103'/1237'/%d'/%d/%d", kpm.identityIndex, usageIndex, index)
}
// GetNextKeyIndex returns the next available key index for the given purpose.
func (kpm *KeyPoolManager) GetNextKeyIndex(purpose KeyPurpose) int {
current := kpm.currentIndices[purpose]
kpm.currentIndices[purpose] = current + 1
return current
}
// SetKeyIndex sets the current key index for the given purpose.
func (kpm *KeyPoolManager) SetKeyIndex(purpose KeyPurpose, index int) {
kpm.currentIndices[purpose] = index
}
// GetCurrentKeyIndex returns the current key index for the given purpose.
func (kpm *KeyPoolManager) GetCurrentKeyIndex(purpose KeyPurpose) int {
return kpm.currentIndices[purpose]
}
// TrustCalculator helps calculate trust scores and inheritance.
type TrustCalculator struct {
acts map[string]*TrustAct
}
// NewTrustCalculator creates a new trust calculator.
func NewTrustCalculator() *TrustCalculator {
return &TrustCalculator{
acts: make(map[string]*TrustAct),
}
}
// AddAct adds a trust act to the calculator.
func (tc *TrustCalculator) AddAct(act *TrustAct) {
key := act.GetTargetPubkey()
tc.acts[key] = act
}
// GetTrustLevel returns the trust level for a given pubkey.
func (tc *TrustCalculator) GetTrustLevel(pubkey string) TrustLevel {
if act, exists := tc.acts[pubkey]; exists {
if !act.IsExpired() {
return act.GetTrustLevel()
}
}
return TrustLevel("")
}
// CalculateInheritedTrust calculates inherited trust through the web of trust.
func (tc *TrustCalculator) CalculateInheritedTrust(
fromPubkey, toPubkey string,
) TrustLevel {
// Direct trust
if directTrust := tc.GetTrustLevel(toPubkey); directTrust != "" {
return directTrust
}
// Look for inherited trust through intermediate nodes
for intermediatePubkey, act := range tc.acts {
if act.IsExpired() {
continue
}
// Check if we trust the intermediate node
intermediateLevel := tc.GetTrustLevel(intermediatePubkey)
if intermediateLevel == "" {
continue
}
// Check if intermediate node trusts the target
targetLevel := tc.GetTrustLevel(toPubkey)
if targetLevel == "" {
continue
}
// Calculate inherited trust level
return tc.combinesTrustLevels(intermediateLevel, targetLevel)
}
return TrustLevel("")
}
// combinesTrustLevels combines two trust levels to calculate inherited trust.
func (tc *TrustCalculator) combinesTrustLevels(level1, level2 TrustLevel) TrustLevel {
// Trust inheritance rules:
// high + high = medium
// high + medium = low
// medium + medium = low
// anything else = no trust
if level1 == TrustLevelHigh && level2 == TrustLevelHigh {
return TrustLevelMedium
}
if (level1 == TrustLevelHigh && level2 == TrustLevelMedium) ||
(level1 == TrustLevelMedium && level2 == TrustLevelHigh) {
return TrustLevelLow
}
if level1 == TrustLevelMedium && level2 == TrustLevelMedium {
return TrustLevelLow
}
return TrustLevel("")
}
// ReplicationFilter helps determine which events should be replicated.
type ReplicationFilter struct {
trustCalculator *TrustCalculator
acts map[string]*TrustAct
}
// NewReplicationFilter creates a new replication filter.
func NewReplicationFilter(trustCalculator *TrustCalculator) *ReplicationFilter {
return &ReplicationFilter{
trustCalculator: trustCalculator,
acts: make(map[string]*TrustAct),
}
}
// AddTrustAct adds a trust act to the filter.
func (rf *ReplicationFilter) AddTrustAct(act *TrustAct) {
rf.acts[act.GetTargetPubkey()] = act
}
// ShouldReplicate determines if an event should be replicated to a target relay.
func (rf *ReplicationFilter) ShouldReplicate(ev *event.E, targetPubkey string) bool {
act, exists := rf.acts[targetPubkey]
if !exists || act.IsExpired() {
return false
}
return act.ShouldReplicate(ev.Kind)
}
// GetReplicationTargets returns all target relays that should receive an event.
func (rf *ReplicationFilter) GetReplicationTargets(ev *event.E) []string {
var targets []string
for pubkey, act := range rf.acts {
if !act.IsExpired() && act.ShouldReplicate(ev.Kind) {
targets = append(targets, pubkey)
}
}
return targets
}
// EventBatcher helps batch events for efficient replication.
type EventBatcher struct {
maxBatchSize int
batches map[string][]*event.E
}
// NewEventBatcher creates a new event batcher.
func NewEventBatcher(maxBatchSize int) *EventBatcher {
if maxBatchSize <= 0 {
maxBatchSize = 100 // Default batch size
}
return &EventBatcher{
maxBatchSize: maxBatchSize,
batches: make(map[string][]*event.E),
}
}
// AddEvent adds an event to the batch for a target relay.
func (eb *EventBatcher) AddEvent(targetRelay string, ev *event.E) {
eb.batches[targetRelay] = append(eb.batches[targetRelay], ev)
}
// GetBatch returns the current batch for a target relay.
func (eb *EventBatcher) GetBatch(targetRelay string) []*event.E {
return eb.batches[targetRelay]
}
// IsBatchFull returns true if the batch for a target relay is full.
func (eb *EventBatcher) IsBatchFull(targetRelay string) bool {
return len(eb.batches[targetRelay]) >= eb.maxBatchSize
}
// FlushBatch returns and clears the batch for a target relay.
func (eb *EventBatcher) FlushBatch(targetRelay string) []*event.E {
batch := eb.batches[targetRelay]
eb.batches[targetRelay] = nil
return batch
}
// GetAllBatches returns all current batches.
func (eb *EventBatcher) GetAllBatches() map[string][]*event.E {
result := make(map[string][]*event.E)
for relay, batch := range eb.batches {
if len(batch) > 0 {
result[relay] = batch
}
}
return result
}
// FlushAllBatches returns and clears all batches.
func (eb *EventBatcher) FlushAllBatches() map[string][]*event.E {
result := eb.GetAllBatches()
eb.batches = make(map[string][]*event.E)
return result
}
// Utility functions
// ParseKindsList parses a comma-separated list of event kinds.
func ParseKindsList(kindsStr string) (kinds []uint16, err error) {
if kindsStr == "" {
return nil, nil
}
kindStrings := strings.Split(kindsStr, ",")
for _, kindStr := range kindStrings {
kindStr = strings.TrimSpace(kindStr)
if kindStr == "" {
continue
}
var kind uint64
if kind, err = strconv.ParseUint(kindStr, 10, 16); chk.E(err) {
return nil, errorf.E("invalid kind: %s", kindStr)
}
kinds = append(kinds, uint16(kind))
}
return
}
// FormatKindsList formats a list of event kinds as a comma-separated string.
func FormatKindsList(kinds []uint16) string {
if len(kinds) == 0 {
return ""
}
var kindStrings []string
for _, kind := range kinds {
kindStrings = append(kindStrings, strconv.FormatUint(uint64(kind), 10))
}
return strings.Join(kindStrings, ",")
}
// GenerateRequestID generates a unique request ID for replication requests.
func GenerateRequestID() (requestID string, err error) {
// Use timestamp + random nonce for uniqueness
timestamp := time.Now().Unix()
var nonce string
if nonce, err = GenerateNonceHex(8); chk.E(err) {
return
}
requestID = fmt.Sprintf("%d-%s", timestamp, nonce)
return
}
// CreateSuccessResponse creates a successful replication response.
func CreateSuccessResponse(
pubkey []byte,
requestID, sourceRelay string,
eventResults []*EventResult,
) (response *DirectoryEventReplicationResponse, err error) {
return NewDirectoryEventReplicationResponse(
pubkey,
requestID,
ReplicationStatusSuccess,
"",
sourceRelay,
eventResults,
)
}
// CreateErrorResponse creates an error replication response.
func CreateErrorResponse(
pubkey []byte,
requestID, sourceRelay, errorMsg string,
) (response *DirectoryEventReplicationResponse, err error) {
return NewDirectoryEventReplicationResponse(
pubkey,
requestID,
ReplicationStatusError,
errorMsg,
sourceRelay,
nil,
)
}
// CreateEventResult creates an event result for a replication response.
func CreateEventResult(eventID string, success bool, errorMsg string) *EventResult {
status := ReplicationStatusSuccess
if !success {
status = ReplicationStatusError
}
return &EventResult{
EventID: eventID,
Status: status,
Error: errorMsg,
}
}

View File

@@ -0,0 +1,368 @@
package directory
import (
"strconv"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// PublicKeyAdvertisement represents a complete Public Key Advertisement event
// (Kind 39103) with typed access to its components.
type PublicKeyAdvertisement struct {
Event *event.E
KeyID string
PublicKey string
Purpose KeyPurpose
Expiry *time.Time
Algorithm string
DerivationPath string
KeyIndex int
IdentityTag *IdentityTag
}
// NewPublicKeyAdvertisement creates a new Public Key Advertisement event.
func NewPublicKeyAdvertisement(
pubkey []byte,
keyID, publicKey string,
purpose KeyPurpose,
expiry *time.Time,
algorithm, derivationPath string,
keyIndex int,
identityTag *IdentityTag,
) (pka *PublicKeyAdvertisement, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if keyID == "" {
return nil, errorf.E("key ID is required")
}
if publicKey == "" {
return nil, errorf.E("public key is required")
}
if len(publicKey) != 64 {
return nil, errorf.E("public key must be 64 hex characters")
}
if err = ValidateKeyPurpose(string(purpose)); chk.E(err) {
return
}
// Expiry is optional, but if provided, must be in the future
if expiry != nil && expiry.Before(time.Now()) {
return nil, errorf.E("expiry time must be in the future")
}
if algorithm == "" {
algorithm = "secp256k1" // Default algorithm
}
if derivationPath == "" {
return nil, errorf.E("derivation path is required")
}
if keyIndex < 0 {
return nil, errorf.E("key index must be non-negative")
}
// Validate identity tag if provided
if identityTag != nil {
if err = identityTag.Validate(); chk.E(err) {
return
}
}
// Create base event
ev := CreateBaseEvent(pubkey, PublicKeyAdvertisementKind)
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(DTag), keyID))
ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), publicKey))
ev.Tags.Append(tag.NewFromAny(string(PurposeTag), string(purpose)))
ev.Tags.Append(tag.NewFromAny(string(AlgorithmTag), algorithm))
ev.Tags.Append(tag.NewFromAny(string(DerivationPathTag), derivationPath))
ev.Tags.Append(tag.NewFromAny(string(KeyIndexTag), strconv.Itoa(keyIndex)))
// Add optional expiry tag
if expiry != nil {
ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10)))
}
// Add identity tag if provided
if identityTag != nil {
ev.Tags.Append(tag.NewFromAny(string(ITag),
identityTag.NPubIdentity,
identityTag.Nonce,
identityTag.Signature))
}
pka = &PublicKeyAdvertisement{
Event: ev,
KeyID: keyID,
PublicKey: publicKey,
Purpose: purpose,
Expiry: expiry,
Algorithm: algorithm,
DerivationPath: derivationPath,
KeyIndex: keyIndex,
IdentityTag: identityTag,
}
return
}
// ParsePublicKeyAdvertisement parses an event into a PublicKeyAdvertisement
// structure with validation.
func ParsePublicKeyAdvertisement(ev *event.E) (pka *PublicKeyAdvertisement, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != PublicKeyAdvertisementKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
PublicKeyAdvertisementKind.K, ev.Kind)
}
// Extract required tags
dTag := ev.Tags.GetFirst(DTag)
if dTag == nil {
return nil, errorf.E("missing d tag")
}
pubkeyTag := ev.Tags.GetFirst(PubkeyTag)
if pubkeyTag == nil {
return nil, errorf.E("missing pubkey tag")
}
purposeTag := ev.Tags.GetFirst(PurposeTag)
if purposeTag == nil {
return nil, errorf.E("missing purpose tag")
}
// Parse optional expiry
var expiry *time.Time
expiryTag := ev.Tags.GetFirst(ExpiryTag)
if expiryTag != nil {
var expiryUnix int64
if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) {
return nil, errorf.E("invalid expiry timestamp: %w", err)
}
expiryTime := time.Unix(expiryUnix, 0)
expiry = &expiryTime
}
algorithmTag := ev.Tags.GetFirst(AlgorithmTag)
if algorithmTag == nil {
return nil, errorf.E("missing algorithm tag")
}
derivationPathTag := ev.Tags.GetFirst(DerivationPathTag)
if derivationPathTag == nil {
return nil, errorf.E("missing derivation_path tag")
}
keyIndexTag := ev.Tags.GetFirst(KeyIndexTag)
if keyIndexTag == nil {
return nil, errorf.E("missing key_index tag")
}
// Validate and parse purpose
purpose := KeyPurpose(purposeTag.Value())
if err = ValidateKeyPurpose(string(purpose)); chk.E(err) {
return
}
// Parse key index
var keyIndex int
if keyIndex, err = strconv.Atoi(string(keyIndexTag.Value())); chk.E(err) {
return nil, errorf.E("invalid key_index: %w", err)
}
// Parse identity tag (I tag)
var identityTag *IdentityTag
iTag := ev.Tags.GetFirst(ITag)
if iTag != nil {
if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
return
}
}
pka = &PublicKeyAdvertisement{
Event: ev,
KeyID: string(dTag.Value()),
PublicKey: string(pubkeyTag.Value()),
Purpose: purpose,
Expiry: expiry,
Algorithm: string(algorithmTag.Value()),
DerivationPath: string(derivationPathTag.Value()),
KeyIndex: keyIndex,
IdentityTag: identityTag,
}
return
}
// Validate performs comprehensive validation of a PublicKeyAdvertisement.
func (pka *PublicKeyAdvertisement) Validate() (err error) {
if pka == nil {
return errorf.E("PublicKeyAdvertisement cannot be nil")
}
if pka.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = pka.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if pka.KeyID == "" {
return errorf.E("key ID is required")
}
if pka.PublicKey == "" {
return errorf.E("public key is required")
}
if len(pka.PublicKey) != 64 {
return errorf.E("public key must be 64 hex characters")
}
if err = ValidateKeyPurpose(string(pka.Purpose)); chk.E(err) {
return
}
// Ensure no more mistakes by correcting field usage comprehensively
// Update relevant parts of the code to use Expiry instead of removed fields.
if pka.Expiry != nil && pka.Expiry.Before(time.Now()) {
return errorf.E("public key advertisement is expired")
}
// Make sure any logic that checks valid periods is now using the created_at timestamp rather than a specific validity period
// Statements using ValidFrom or ValidUntil should be revised or removed according to the new logic.
if pka.Algorithm == "" {
return errorf.E("algorithm is required")
}
if pka.DerivationPath == "" {
return errorf.E("derivation path is required")
}
if pka.KeyIndex < 0 {
return errorf.E("key index must be non-negative")
}
// Validate identity tag if present
if pka.IdentityTag != nil {
if err = pka.IdentityTag.Validate(); chk.E(err) {
return
}
}
return nil
}
// IsValid returns true if the key is currently valid (within its validity period).
func (pka *PublicKeyAdvertisement) IsValid() bool {
if pka.Expiry == nil {
return false
}
return time.Now().Before(*pka.Expiry)
}
// IsExpired returns true if the key has expired.
func (pka *PublicKeyAdvertisement) IsExpired() bool {
if pka.Expiry == nil {
return false
}
return time.Now().After(*pka.Expiry)
}
// IsNotYetValid returns true if the key is not yet valid.
func (pka *PublicKeyAdvertisement) IsNotYetValid() bool {
if pka.Expiry == nil {
return true // Consider valid if no expiry is set
}
return time.Now().Before(*pka.Expiry)
}
// TimeUntilExpiry returns the duration until the key expires.
// Returns 0 if already expired.
func (pka *PublicKeyAdvertisement) TimeUntilExpiry() time.Duration {
if pka.Expiry == nil {
return 0
}
if pka.IsExpired() {
return 0
}
return time.Until(*pka.Expiry)
}
// TimeUntilValid returns the duration until the key becomes valid.
// Returns 0 if already valid or expired.
func (pka *PublicKeyAdvertisement) TimeUntilValid() time.Duration {
if !pka.IsNotYetValid() {
return 0
}
return time.Until(*pka.Expiry)
}
// GetKeyID returns the unique key identifier.
func (pka *PublicKeyAdvertisement) GetKeyID() string {
return pka.KeyID
}
// GetPublicKey returns the hex-encoded public key.
func (pka *PublicKeyAdvertisement) GetPublicKey() string {
return pka.PublicKey
}
// GetPurpose returns the key purpose.
func (pka *PublicKeyAdvertisement) GetPurpose() KeyPurpose {
return pka.Purpose
}
// GetAlgorithm returns the cryptographic algorithm.
func (pka *PublicKeyAdvertisement) GetAlgorithm() string {
return pka.Algorithm
}
// GetDerivationPath returns the BIP32 derivation path.
func (pka *PublicKeyAdvertisement) GetDerivationPath() string {
return pka.DerivationPath
}
// GetKeyIndex returns the key index from the derivation path.
func (pka *PublicKeyAdvertisement) GetKeyIndex() int {
return pka.KeyIndex
}
// GetIdentityTag returns the identity tag, or nil if not present.
func (pka *PublicKeyAdvertisement) GetIdentityTag() *IdentityTag {
return pka.IdentityTag
}
// HasPurpose returns true if the key has the specified purpose.
func (pka *PublicKeyAdvertisement) HasPurpose(purpose KeyPurpose) bool {
return pka.Purpose == purpose
}
// IsSigningKey returns true if this is a signing key.
func (pka *PublicKeyAdvertisement) IsSigningKey() bool {
return pka.Purpose == KeyPurposeSigning
}
// IsEncryptionKey returns true if this is an encryption key.
func (pka *PublicKeyAdvertisement) IsEncryptionKey() bool {
return pka.Purpose == KeyPurposeEncryption
}
// IsDelegationKey returns true if this is a delegation key.
func (pka *PublicKeyAdvertisement) IsDelegationKey() bool {
return pka.Purpose == KeyPurposeDelegation
}

View File

@@ -0,0 +1,264 @@
package directory
import (
"encoding/json"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// RelayIdentityContent represents the JSON content of a Relay Identity
// Announcement event (Kind 39100).
type RelayIdentityContent struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Contact string `json:"contact,omitempty"`
}
// RelayIdentityAnnouncement represents a complete Relay Identity Announcement
// event with typed access to its components.
type RelayIdentityAnnouncement struct {
Event *event.E
Content *RelayIdentityContent
RelayURL string
SigningKey string
EncryptionKey string
Version string
NIP11URL string
}
// NewRelayIdentityAnnouncement creates a new Relay Identity Announcement event.
func NewRelayIdentityAnnouncement(
pubkey []byte,
name, description, contact string,
relayURL, signingKey, encryptionKey, version, nip11URL string,
) (ria *RelayIdentityAnnouncement, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if name == "" {
return nil, errorf.E("name is required")
}
if relayURL == "" {
return nil, errorf.E("relay URL is required")
}
if signingKey == "" {
return nil, errorf.E("signing key is required")
}
if encryptionKey == "" {
return nil, errorf.E("encryption key is required")
}
if version == "" {
version = "1" // Default version
}
if nip11URL == "" {
return nil, errorf.E("NIP-11 URL is required")
}
// Create content
content := &RelayIdentityContent{
Name: name,
Description: description,
Contact: contact,
}
// Marshal content to JSON
var contentBytes []byte
if contentBytes, err = json.Marshal(content); chk.E(err) {
return
}
// Create base event
ev := CreateBaseEvent(pubkey, RelayIdentityAnnouncementKind)
ev.Content = contentBytes
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(DTag), "relay-identity"))
ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
ev.Tags.Append(tag.NewFromAny(string(SigningKeyTag), signingKey))
ev.Tags.Append(tag.NewFromAny(string(EncryptionKeyTag), encryptionKey))
ev.Tags.Append(tag.NewFromAny(string(VersionTag), version))
ev.Tags.Append(tag.NewFromAny(string(NIP11URLTag), nip11URL))
ria = &RelayIdentityAnnouncement{
Event: ev,
Content: content,
RelayURL: relayURL,
SigningKey: signingKey,
EncryptionKey: encryptionKey,
Version: version,
NIP11URL: nip11URL,
}
return
}
// ParseRelayIdentityAnnouncement parses an event into a RelayIdentityAnnouncement
// structure with validation.
func ParseRelayIdentityAnnouncement(ev *event.E) (ria *RelayIdentityAnnouncement, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != RelayIdentityAnnouncementKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
RelayIdentityAnnouncementKind.K, ev.Kind)
}
// Parse content
var content RelayIdentityContent
if len(ev.Content) > 0 {
if err = json.Unmarshal(ev.Content, &content); chk.E(err) {
return nil, errorf.E("failed to parse content: %w", err)
}
}
// Extract required tags
dTag := ev.Tags.GetFirst(DTag)
if dTag == nil || string(dTag.Value()) != "relay-identity" {
return nil, errorf.E("missing or invalid d tag")
}
relayTag := ev.Tags.GetFirst(RelayTag)
if relayTag == nil {
return nil, errorf.E("missing relay tag")
}
signingKeyTag := ev.Tags.GetFirst(SigningKeyTag)
if signingKeyTag == nil {
return nil, errorf.E("missing signing_key tag")
}
encryptionKeyTag := ev.Tags.GetFirst(EncryptionKeyTag)
if encryptionKeyTag == nil {
return nil, errorf.E("missing encryption_key tag")
}
versionTag := ev.Tags.GetFirst(VersionTag)
if versionTag == nil {
return nil, errorf.E("missing version tag")
}
nip11URLTag := ev.Tags.GetFirst(NIP11URLTag)
if nip11URLTag == nil {
return nil, errorf.E("missing nip11_url tag")
}
ria = &RelayIdentityAnnouncement{
Event: ev,
Content: &content,
RelayURL: string(relayTag.Value()),
SigningKey: string(signingKeyTag.Value()),
EncryptionKey: string(encryptionKeyTag.Value()),
Version: string(versionTag.Value()),
NIP11URL: string(nip11URLTag.Value()),
}
return
}
// Validate performs comprehensive validation of a RelayIdentityAnnouncement.
func (ria *RelayIdentityAnnouncement) Validate() (err error) {
if ria == nil {
return errorf.E("RelayIdentityAnnouncement cannot be nil")
}
if ria.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = ria.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if ria.Content.Name == "" {
return errorf.E("name is required")
}
if ria.RelayURL == "" {
return errorf.E("relay URL is required")
}
if ria.SigningKey == "" {
return errorf.E("signing key is required")
}
if ria.EncryptionKey == "" {
return errorf.E("encryption key is required")
}
if ria.Version == "" {
return errorf.E("version is required")
}
if ria.NIP11URL == "" {
return errorf.E("NIP-11 URL is required")
}
// Validate hex-encoded keys (should be 64 characters for 32-byte keys)
if len(ria.SigningKey) != 64 {
return errorf.E("signing key must be 64 hex characters")
}
if len(ria.EncryptionKey) != 64 {
return errorf.E("encryption key must be 64 hex characters")
}
return nil
}
// GetRelayURL returns the relay WebSocket URL.
func (ria *RelayIdentityAnnouncement) GetRelayURL() string {
return ria.RelayURL
}
// GetSigningKey returns the hex-encoded signing public key.
func (ria *RelayIdentityAnnouncement) GetSigningKey() string {
return ria.SigningKey
}
// GetEncryptionKey returns the hex-encoded encryption public key.
func (ria *RelayIdentityAnnouncement) GetEncryptionKey() string {
return ria.EncryptionKey
}
// GetVersion returns the protocol version.
func (ria *RelayIdentityAnnouncement) GetVersion() string {
return ria.Version
}
// GetNIP11URL returns the NIP-11 information document URL.
func (ria *RelayIdentityAnnouncement) GetNIP11URL() string {
return ria.NIP11URL
}
// GetName returns the relay name from the content.
func (ria *RelayIdentityAnnouncement) GetName() string {
if ria.Content == nil {
return ""
}
return ria.Content.Name
}
// GetDescription returns the relay description from the content.
func (ria *RelayIdentityAnnouncement) GetDescription() string {
if ria.Content == nil {
return ""
}
return ria.Content.Description
}
// GetContact returns the relay contact information from the content.
func (ria *RelayIdentityAnnouncement) GetContact() string {
if ria.Content == nil {
return ""
}
return ria.Content.Contact
}

View File

@@ -0,0 +1,278 @@
package directory
import (
"encoding/json"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// ReplicationRequestContent represents the JSON content of a Directory Event
// Replication Request event (Kind 39104).
type ReplicationRequestContent struct {
Events []*event.E `json:"events"`
}
// DirectoryEventReplicationRequest represents a complete Directory Event
// Replication Request event (Kind 39104) with typed access to its components.
type DirectoryEventReplicationRequest struct {
Event *event.E
Content *ReplicationRequestContent
RequestID string
TargetRelay string
}
// NewDirectoryEventReplicationRequest creates a new Directory Event Replication
// Request event.
func NewDirectoryEventReplicationRequest(
pubkey []byte,
requestID, targetRelay string,
events []*event.E,
) (derr *DirectoryEventReplicationRequest, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if requestID == "" {
return nil, errorf.E("request ID is required")
}
if targetRelay == "" {
return nil, errorf.E("target relay is required")
}
if len(events) == 0 {
return nil, errorf.E("at least one event is required")
}
// Validate all events
for i, ev := range events {
if ev == nil {
return nil, errorf.E("event %d cannot be nil", i)
}
// Verify event signature
if _, err = ev.Verify(); chk.E(err) {
return nil, errorf.E("invalid signature for event %d: %w", i, err)
}
}
// Create content
content := &ReplicationRequestContent{
Events: events,
}
// Marshal content to JSON
var contentBytes []byte
if contentBytes, err = json.Marshal(content); chk.E(err) {
return
}
// Create base event
ev := CreateBaseEvent(pubkey, DirectoryEventReplicationRequestKind)
ev.Content = contentBytes
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(RequestIDTag), requestID))
ev.Tags.Append(tag.NewFromAny(string(RelayTag), targetRelay))
derr = &DirectoryEventReplicationRequest{
Event: ev,
Content: content,
RequestID: requestID,
TargetRelay: targetRelay,
}
return
}
// ParseDirectoryEventReplicationRequest parses an event into a
// DirectoryEventReplicationRequest structure with validation.
func ParseDirectoryEventReplicationRequest(ev *event.E) (derr *DirectoryEventReplicationRequest, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != DirectoryEventReplicationRequestKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
DirectoryEventReplicationRequestKind.K, ev.Kind)
}
// Parse content
var content ReplicationRequestContent
if len(ev.Content) > 0 {
if err = json.Unmarshal(ev.Content, &content); chk.E(err) {
return nil, errorf.E("failed to parse content: %w", err)
}
}
// Extract required tags
requestIDTag := ev.Tags.GetFirst(RequestIDTag)
if requestIDTag == nil {
return nil, errorf.E("missing request_id tag")
}
relayTag := ev.Tags.GetFirst(RelayTag)
if relayTag == nil {
return nil, errorf.E("missing relay tag")
}
derr = &DirectoryEventReplicationRequest{
Event: ev,
Content: &content,
RequestID: string(requestIDTag.Value()),
TargetRelay: string(relayTag.Value()),
}
return
}
// Validate performs comprehensive validation of a DirectoryEventReplicationRequest.
func (derr *DirectoryEventReplicationRequest) Validate() (err error) {
if derr == nil {
return errorf.E("DirectoryEventReplicationRequest cannot be nil")
}
if derr.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = derr.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if derr.RequestID == "" {
return errorf.E("request ID is required")
}
if derr.TargetRelay == "" {
return errorf.E("target relay is required")
}
if derr.Content == nil {
return errorf.E("content cannot be nil")
}
if len(derr.Content.Events) == 0 {
return errorf.E("at least one event is required")
}
// Validate all events in the request
for i, ev := range derr.Content.Events {
if ev == nil {
return errorf.E("event %d cannot be nil", i)
}
// Verify event signature
if _, err = ev.Verify(); chk.E(err) {
return errorf.E("invalid signature for event %d: %w", i, err)
}
}
return nil
}
// GetRequestID returns the unique request identifier.
func (derr *DirectoryEventReplicationRequest) GetRequestID() string {
return derr.RequestID
}
// GetTargetRelay returns the target relay URL.
func (derr *DirectoryEventReplicationRequest) GetTargetRelay() string {
return derr.TargetRelay
}
// GetEvents returns the list of events to replicate.
func (derr *DirectoryEventReplicationRequest) GetEvents() []*event.E {
if derr.Content == nil {
return nil
}
return derr.Content.Events
}
// GetEventCount returns the number of events in the request.
func (derr *DirectoryEventReplicationRequest) GetEventCount() int {
if derr.Content == nil {
return 0
}
return len(derr.Content.Events)
}
// HasEvents returns true if the request contains events.
func (derr *DirectoryEventReplicationRequest) HasEvents() bool {
return derr.GetEventCount() > 0
}
// GetEventByIndex returns the event at the specified index, or nil if out of bounds.
func (derr *DirectoryEventReplicationRequest) GetEventByIndex(index int) *event.E {
events := derr.GetEvents()
if index < 0 || index >= len(events) {
return nil
}
return events[index]
}
// ContainsEventKind returns true if the request contains events of the specified kind.
func (derr *DirectoryEventReplicationRequest) ContainsEventKind(kind uint16) bool {
for _, ev := range derr.GetEvents() {
if ev.Kind == kind {
return true
}
}
return false
}
// GetEventsByKind returns all events of the specified kind.
func (derr *DirectoryEventReplicationRequest) GetEventsByKind(kind uint16) []*event.E {
var result []*event.E
for _, ev := range derr.GetEvents() {
if ev.Kind == kind {
result = append(result, ev)
}
}
return result
}
// GetDirectoryEvents returns only the directory events from the request.
func (derr *DirectoryEventReplicationRequest) GetDirectoryEvents() []*event.E {
var result []*event.E
for _, ev := range derr.GetEvents() {
if IsDirectoryEventKind(ev.Kind) {
result = append(result, ev)
}
}
return result
}
// GetNonDirectoryEvents returns only the non-directory events from the request.
func (derr *DirectoryEventReplicationRequest) GetNonDirectoryEvents() []*event.E {
var result []*event.E
for _, ev := range derr.GetEvents() {
if !IsDirectoryEventKind(ev.Kind) {
result = append(result, ev)
}
}
return result
}
// GetEventsByAuthor returns all events from the specified author.
func (derr *DirectoryEventReplicationRequest) GetEventsByAuthor(pubkey []byte) []*event.E {
var result []*event.E
for _, ev := range derr.GetEvents() {
if len(ev.Pubkey) == len(pubkey) {
match := true
for i := range pubkey {
if ev.Pubkey[i] != pubkey[i] {
match = false
break
}
}
if match {
result = append(result, ev)
}
}
}
return result
}

View File

@@ -0,0 +1,367 @@
package directory
import (
"encoding/json"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// EventResult represents the result of processing a single event in a
// replication request.
type EventResult struct {
EventID string `json:"event_id"`
Status ReplicationStatus `json:"status"`
Error string `json:"error,omitempty"`
}
// ReplicationResponseContent represents the JSON content of a Directory Event
// Replication Response event (Kind 39105).
type ReplicationResponseContent struct {
RequestID string `json:"request_id"`
Results []*EventResult `json:"results"`
}
// DirectoryEventReplicationResponse represents a complete Directory Event
// Replication Response event (Kind 39105) with typed access to its components.
type DirectoryEventReplicationResponse struct {
Event *event.E
Content *ReplicationResponseContent
RequestID string
Status ReplicationStatus
ErrorMsg string
SourceRelay string
}
// NewDirectoryEventReplicationResponse creates a new Directory Event Replication
// Response event.
func NewDirectoryEventReplicationResponse(
pubkey []byte,
requestID string,
status ReplicationStatus,
errorMsg, sourceRelay string,
results []*EventResult,
) (derr *DirectoryEventReplicationResponse, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if requestID == "" {
return nil, errorf.E("request ID is required")
}
if err = ValidateReplicationStatus(string(status)); chk.E(err) {
return
}
if sourceRelay == "" {
return nil, errorf.E("source relay is required")
}
// Create content
content := &ReplicationResponseContent{
RequestID: requestID,
Results: results,
}
// Marshal content to JSON
var contentBytes []byte
if contentBytes, err = json.Marshal(content); chk.E(err) {
return
}
// Create base event
ev := CreateBaseEvent(pubkey, DirectoryEventReplicationResponseKind)
ev.Content = contentBytes
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(RequestIDTag), requestID))
ev.Tags.Append(tag.NewFromAny(string(StatusTag), string(status)))
ev.Tags.Append(tag.NewFromAny(string(RelayTag), sourceRelay))
// Add optional error tag
if errorMsg != "" {
ev.Tags.Append(tag.NewFromAny(string(ErrorTag), errorMsg))
}
derr = &DirectoryEventReplicationResponse{
Event: ev,
Content: content,
RequestID: requestID,
Status: status,
ErrorMsg: errorMsg,
SourceRelay: sourceRelay,
}
return
}
// ParseDirectoryEventReplicationResponse parses an event into a
// DirectoryEventReplicationResponse structure with validation.
func ParseDirectoryEventReplicationResponse(ev *event.E) (derr *DirectoryEventReplicationResponse, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != DirectoryEventReplicationResponseKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
DirectoryEventReplicationResponseKind.K, ev.Kind)
}
// Parse content
var content ReplicationResponseContent
if len(ev.Content) > 0 {
if err = json.Unmarshal(ev.Content, &content); chk.E(err) {
return nil, errorf.E("failed to parse content: %w", err)
}
}
// Extract required tags
requestIDTag := ev.Tags.GetFirst(RequestIDTag)
if requestIDTag == nil {
return nil, errorf.E("missing request_id tag")
}
statusTag := ev.Tags.GetFirst(StatusTag)
if statusTag == nil {
return nil, errorf.E("missing status tag")
}
relayTag := ev.Tags.GetFirst(RelayTag)
if relayTag == nil {
return nil, errorf.E("missing relay tag")
}
// Validate status
status := ReplicationStatus(statusTag.Value())
if err = ValidateReplicationStatus(string(status)); chk.E(err) {
return
}
// Extract optional error tag
var errorMsg string
errorTag := ev.Tags.GetFirst(ErrorTag)
if errorTag != nil {
errorMsg = string(errorTag.Value())
}
derr = &DirectoryEventReplicationResponse{
Event: ev,
Content: &content,
RequestID: string(requestIDTag.Value()),
Status: status,
ErrorMsg: errorMsg,
SourceRelay: string(relayTag.Value()),
}
return
}
// Validate performs comprehensive validation of a DirectoryEventReplicationResponse.
func (derr *DirectoryEventReplicationResponse) Validate() (err error) {
if derr == nil {
return errorf.E("DirectoryEventReplicationResponse cannot be nil")
}
if derr.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = derr.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if derr.RequestID == "" {
return errorf.E("request ID is required")
}
if err = ValidateReplicationStatus(string(derr.Status)); chk.E(err) {
return
}
if derr.SourceRelay == "" {
return errorf.E("source relay is required")
}
if derr.Content == nil {
return errorf.E("content cannot be nil")
}
// Validate that content request ID matches tag request ID
if derr.Content.RequestID != derr.RequestID {
return errorf.E("content request ID does not match tag request ID")
}
// Validate event results
for i, result := range derr.Content.Results {
if result == nil {
return errorf.E("result %d cannot be nil", i)
}
if result.EventID == "" {
return errorf.E("result %d missing event ID", i)
}
if err = ValidateReplicationStatus(string(result.Status)); chk.E(err) {
return errorf.E("result %d has invalid status: %w", i, err)
}
}
return nil
}
// NewEventResult creates a new EventResult.
func NewEventResult(eventID string, status ReplicationStatus, errorMsg string) *EventResult {
return &EventResult{
EventID: eventID,
Status: status,
Error: errorMsg,
}
}
// GetRequestID returns the request ID this response corresponds to.
func (derr *DirectoryEventReplicationResponse) GetRequestID() string {
return derr.RequestID
}
// GetStatus returns the overall replication status.
func (derr *DirectoryEventReplicationResponse) GetStatus() ReplicationStatus {
return derr.Status
}
// GetErrorMsg returns the error message, if any.
func (derr *DirectoryEventReplicationResponse) GetErrorMsg() string {
return derr.ErrorMsg
}
// GetSourceRelay returns the relay that sent this response.
func (derr *DirectoryEventReplicationResponse) GetSourceRelay() string {
return derr.SourceRelay
}
// GetResults returns the list of individual event results.
func (derr *DirectoryEventReplicationResponse) GetResults() []*EventResult {
if derr.Content == nil {
return nil
}
return derr.Content.Results
}
// GetResultCount returns the number of event results.
func (derr *DirectoryEventReplicationResponse) GetResultCount() int {
if derr.Content == nil {
return 0
}
return len(derr.Content.Results)
}
// HasResults returns true if the response contains event results.
func (derr *DirectoryEventReplicationResponse) HasResults() bool {
return derr.GetResultCount() > 0
}
// IsSuccess returns true if the overall replication was successful.
func (derr *DirectoryEventReplicationResponse) IsSuccess() bool {
return derr.Status == ReplicationStatusSuccess
}
// IsError returns true if the overall replication failed.
func (derr *DirectoryEventReplicationResponse) IsError() bool {
return derr.Status == ReplicationStatusError
}
// IsPending returns true if the replication is still pending.
func (derr *DirectoryEventReplicationResponse) IsPending() bool {
return derr.Status == ReplicationStatusPending
}
// GetSuccessfulResults returns all results with success status.
func (derr *DirectoryEventReplicationResponse) GetSuccessfulResults() []*EventResult {
var results []*EventResult
for _, result := range derr.GetResults() {
if result.Status == ReplicationStatusSuccess {
results = append(results, result)
}
}
return results
}
// GetFailedResults returns all results with error status.
func (derr *DirectoryEventReplicationResponse) GetFailedResults() []*EventResult {
var results []*EventResult
for _, result := range derr.GetResults() {
if result.Status == ReplicationStatusError {
results = append(results, result)
}
}
return results
}
// GetPendingResults returns all results with pending status.
func (derr *DirectoryEventReplicationResponse) GetPendingResults() []*EventResult {
var results []*EventResult
for _, result := range derr.GetResults() {
if result.Status == ReplicationStatusPending {
results = append(results, result)
}
}
return results
}
// GetResultByEventID returns the result for a specific event ID, or nil if not found.
func (derr *DirectoryEventReplicationResponse) GetResultByEventID(eventID string) *EventResult {
for _, result := range derr.GetResults() {
if result.EventID == eventID {
return result
}
}
return nil
}
// GetSuccessCount returns the number of successfully replicated events.
func (derr *DirectoryEventReplicationResponse) GetSuccessCount() int {
return len(derr.GetSuccessfulResults())
}
// GetFailureCount returns the number of failed event replications.
func (derr *DirectoryEventReplicationResponse) GetFailureCount() int {
return len(derr.GetFailedResults())
}
// GetPendingCount returns the number of pending event replications.
func (derr *DirectoryEventReplicationResponse) GetPendingCount() int {
return len(derr.GetPendingResults())
}
// GetSuccessRate returns the success rate as a percentage (0-100).
func (derr *DirectoryEventReplicationResponse) GetSuccessRate() float64 {
total := derr.GetResultCount()
if total == 0 {
return 0
}
return float64(derr.GetSuccessCount()) / float64(total) * 100
}
// EventResult methods
// IsSuccess returns true if this event result was successful.
func (er *EventResult) IsSuccess() bool {
return er.Status == ReplicationStatusSuccess
}
// IsError returns true if this event result failed.
func (er *EventResult) IsError() bool {
return er.Status == ReplicationStatusError
}
// IsPending returns true if this event result is pending.
func (er *EventResult) IsPending() bool {
return er.Status == ReplicationStatusPending
}
// HasError returns true if this event result has an error message.
func (er *EventResult) HasError() bool {
return er.Error != ""
}

View File

@@ -0,0 +1,378 @@
package directory
import (
"strconv"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
)
// TrustAct represents a complete Trust Act event (Kind 39101)
// with typed access to its components.
type TrustAct struct {
Event *event.E
TargetPubkey string
TrustLevel TrustLevel
RelayURL string
Expiry *time.Time
Reason TrustReason
ReplicationKinds []uint16
IdentityTag *IdentityTag
}
// IdentityTag represents the I tag with npub identity and proof-of-control.
type IdentityTag struct {
NPubIdentity string
Nonce string
Signature string
}
// NewTrustAct creates a new Trust Act event.
func NewTrustAct(
pubkey []byte,
targetPubkey string,
trustLevel TrustLevel,
relayURL string,
expiry *time.Time,
reason TrustReason,
replicationKinds []uint16,
identityTag *IdentityTag,
) (ta *TrustAct, err error) {
// Validate required fields
if len(pubkey) != 32 {
return nil, errorf.E("pubkey must be 32 bytes")
}
if targetPubkey == "" {
return nil, errorf.E("target pubkey is required")
}
if len(targetPubkey) != 64 {
return nil, errorf.E("target pubkey must be 64 hex characters")
}
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
return
}
if relayURL == "" {
return nil, errorf.E("relay URL is required")
}
// Create base event
ev := CreateBaseEvent(pubkey, TrustActKind)
// Add required tags
ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), targetPubkey))
ev.Tags.Append(tag.NewFromAny(string(TrustLevelTag), string(trustLevel)))
ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
// Add optional expiry
if expiry != nil {
ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10)))
}
// Add reason
if reason != "" {
ev.Tags.Append(tag.NewFromAny(string(ReasonTag), string(reason)))
}
// Add replication kinds (K tag)
if len(replicationKinds) > 0 {
var kindStrings []string
for _, k := range replicationKinds {
kindStrings = append(kindStrings, strconv.FormatUint(uint64(k), 10))
}
ev.Tags.Append(tag.NewFromAny(string(KTag), strings.Join(kindStrings, ",")))
}
// Add identity tag if provided
if identityTag != nil {
if err = identityTag.Validate(); chk.E(err) {
return
}
ev.Tags.Append(tag.NewFromAny(string(ITag),
identityTag.NPubIdentity,
identityTag.Nonce,
identityTag.Signature))
}
ta = &TrustAct{
Event: ev,
TargetPubkey: targetPubkey,
TrustLevel: trustLevel,
RelayURL: relayURL,
Expiry: expiry,
Reason: reason,
ReplicationKinds: replicationKinds,
IdentityTag: identityTag,
}
return
}
// ParseTrustAct parses an event into a TrustAct structure
// with validation.
func ParseTrustAct(ev *event.E) (ta *TrustAct, err error) {
if ev == nil {
return nil, errorf.E("event cannot be nil")
}
// Validate event kind
if ev.Kind != TrustActKind.K {
return nil, errorf.E("invalid event kind: expected %d, got %d",
TrustActKind.K, ev.Kind)
}
// Extract required tags
pTag := ev.Tags.GetFirst(PubkeyTag)
if pTag == nil {
return nil, errorf.E("missing p tag")
}
trustLevelTag := ev.Tags.GetFirst(TrustLevelTag)
if trustLevelTag == nil {
return nil, errorf.E("missing trust_level tag")
}
relayTag := ev.Tags.GetFirst(RelayTag)
if relayTag == nil {
return nil, errorf.E("missing relay tag")
}
// Validate trust level
trustLevel := TrustLevel(trustLevelTag.Value())
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
return
}
// Parse optional expiry
var expiry *time.Time
expiryTag := ev.Tags.GetFirst(ExpiryTag)
if expiryTag != nil {
var expiryUnix int64
if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) {
return nil, errorf.E("invalid expiry timestamp: %w", err)
}
expiryTime := time.Unix(expiryUnix, 0)
expiry = &expiryTime
}
// Parse optional reason
var reason TrustReason
reasonTag := ev.Tags.GetFirst(ReasonTag)
if reasonTag != nil {
reason = TrustReason(reasonTag.Value())
}
// Parse replication kinds (K tag)
var replicationKinds []uint16
kTag := ev.Tags.GetFirst(KTag)
if kTag != nil {
kindStrings := strings.Split(string(kTag.Value()), ",")
for _, kindStr := range kindStrings {
kindStr = strings.TrimSpace(kindStr)
if kindStr == "" {
continue
}
var kind uint64
if kind, err = strconv.ParseUint(kindStr, 10, 16); chk.E(err) {
return nil, errorf.E("invalid kind in K tag: %s", kindStr)
}
replicationKinds = append(replicationKinds, uint16(kind))
}
}
// Parse identity tag (I tag)
var identityTag *IdentityTag
iTag := ev.Tags.GetFirst(ITag)
if iTag != nil {
if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
return
}
}
ta = &TrustAct{
Event: ev,
TargetPubkey: string(pTag.Value()),
TrustLevel: trustLevel,
RelayURL: string(relayTag.Value()),
Expiry: expiry,
Reason: reason,
ReplicationKinds: replicationKinds,
IdentityTag: identityTag,
}
return
}
// ParseIdentityTag parses an I tag into an IdentityTag structure.
func ParseIdentityTag(t *tag.T) (it *IdentityTag, err error) {
if t == nil {
return nil, errorf.E("tag cannot be nil")
}
if t.Len() < 4 {
return nil, errorf.E("I tag must have at least 4 elements")
}
// First element should be "I"
if string(t.T[0]) != "I" {
return nil, errorf.E("invalid I tag key")
}
it = &IdentityTag{
NPubIdentity: string(t.T[1]),
Nonce: string(t.T[2]),
Signature: string(t.T[3]),
}
if err = it.Validate(); chk.E(err) {
return nil, err
}
return it, nil
}
// Validate performs validation of an IdentityTag.
func (it *IdentityTag) Validate() (err error) {
if it == nil {
return errorf.E("IdentityTag cannot be nil")
}
if it.NPubIdentity == "" {
return errorf.E("npub identity is required")
}
if !strings.HasPrefix(it.NPubIdentity, "npub1") {
return errorf.E("identity must be npub-encoded")
}
if it.Nonce == "" {
return errorf.E("nonce is required")
}
if len(it.Nonce) < 32 { // Minimum 16 bytes hex-encoded
return errorf.E("nonce must be at least 16 bytes (32 hex characters)")
}
if it.Signature == "" {
return errorf.E("signature is required")
}
if len(it.Signature) != 128 { // 64 bytes hex-encoded
return errorf.E("signature must be 64 bytes (128 hex characters)")
}
return nil
}
// Validate performs comprehensive validation of a TrustAct.
func (ta *TrustAct) Validate() (err error) {
if ta == nil {
return errorf.E("TrustAct cannot be nil")
}
if ta.Event == nil {
return errorf.E("event cannot be nil")
}
// Validate event signature
if _, err = ta.Event.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate required fields
if ta.TargetPubkey == "" {
return errorf.E("target pubkey is required")
}
if len(ta.TargetPubkey) != 64 {
return errorf.E("target pubkey must be 64 hex characters")
}
if err = ValidateTrustLevel(string(ta.TrustLevel)); chk.E(err) {
return
}
if ta.RelayURL == "" {
return errorf.E("relay URL is required")
}
// Validate expiry if present
if ta.Expiry != nil && ta.Expiry.Before(time.Now()) {
return errorf.E("trust act has expired")
}
// Validate identity tag if present
if ta.IdentityTag != nil {
if err = ta.IdentityTag.Validate(); chk.E(err) {
return
}
}
return nil
}
// IsExpired returns true if the trust act has expired.
func (ta *TrustAct) IsExpired() bool {
return ta.Expiry != nil && ta.Expiry.Before(time.Now())
}
// HasReplicationKind returns true if the act includes the specified
// kind for replication.
func (ta *TrustAct) HasReplicationKind(kind uint16) bool {
for _, k := range ta.ReplicationKinds {
if k == kind {
return true
}
}
return false
}
// ShouldReplicate returns true if an event of the given kind should be
// replicated based on this trust act.
func (ta *TrustAct) ShouldReplicate(kind uint16) bool {
// Directory events are always replicated
if IsDirectoryEventKind(kind) {
return true
}
// Check if kind is in the replication list
return ta.HasReplicationKind(kind)
}
// GetTargetPubkey returns the target relay's public key.
func (ta *TrustAct) GetTargetPubkey() string {
return ta.TargetPubkey
}
// GetTrustLevel returns the trust level.
func (ta *TrustAct) GetTrustLevel() TrustLevel {
return ta.TrustLevel
}
// GetRelayURL returns the target relay's URL.
func (ta *TrustAct) GetRelayURL() string {
return ta.RelayURL
}
// GetExpiry returns the expiry time, or nil if no expiry is set.
func (ta *TrustAct) GetExpiry() *time.Time {
return ta.Expiry
}
// GetReason returns the reason for the trust relationship.
func (ta *TrustAct) GetReason() TrustReason {
return ta.Reason
}
// GetReplicationKinds returns the list of event kinds to replicate.
func (ta *TrustAct) GetReplicationKinds() []uint16 {
return ta.ReplicationKinds
}
// GetIdentityTag returns the identity tag, or nil if not present.
func (ta *TrustAct) GetIdentityTag() *IdentityTag {
return ta.IdentityTag
}

View File

@@ -0,0 +1,205 @@
// Package directory provides data structures and validation for the distributed
// directory consensus protocol as defined in NIP-XX.
//
// This package implements message encoding and validation for the following
// event kinds:
// - 39100: Relay Identity Announcement
// - 39101: Trust Act
// - 39102: Group Tag Act
// - 39103: Public Key Advertisement
// - 39104: Directory Event Replication Request
// - 39105: Directory Event Replication Response
//
// # Legal Concept of Acts
//
// The term "act" in this protocol draws from legal terminology, where an act
// represents a formal declaration or testimony that has legal significance.
// Similar to legal instruments such as:
//
// - Deed Poll: A legal document binding one party to a particular course of action
// - Witness Testimony: A formal statement given under oath as evidence
// - Affidavit: A written statement confirmed by oath for use as evidence
//
// In the context of this protocol, acts serve as cryptographically signed
// declarations that establish trust relationships, group memberships, or other
// formal statements within the relay consortium. Like their legal counterparts,
// these acts:
//
// - Are formally structured with specific required elements
// - Carry the authority and responsibility of the signing party
// - Create binding relationships or obligations within the consortium
// - Can be verified for authenticity through cryptographic signatures
// - May have expiration dates or other temporal constraints
//
// This legal framework provides a conceptual foundation for understanding the
// formal nature and binding character of consortium declarations.
package directory
import (
"crypto/rand"
"encoding/hex"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/kind"
"next.orly.dev/pkg/encoders/tag"
)
// Event kinds for the distributed directory consensus protocol
var (
RelayIdentityAnnouncementKind = kind.New(39100)
TrustActKind = kind.New(39101)
GroupTagActKind = kind.New(39102)
PublicKeyAdvertisementKind = kind.New(39103)
DirectoryEventReplicationRequestKind = kind.New(39104)
DirectoryEventReplicationResponseKind = kind.New(39105)
)
// Common tag names used across directory protocol messages
var (
DTag = []byte("d")
RelayTag = []byte("relay")
SigningKeyTag = []byte("signing_key")
EncryptionKeyTag = []byte("encryption_key")
VersionTag = []byte("version")
NIP11URLTag = []byte("nip11_url")
PubkeyTag = []byte("p")
TrustLevelTag = []byte("trust_level")
ExpiryTag = []byte("expiry")
ReasonTag = []byte("reason")
KTag = []byte("K")
ITag = []byte("I")
GroupTagTag = []byte("group_tag")
ActorTag = []byte("actor")
ConfidenceTag = []byte("confidence")
PurposeTag = []byte("purpose")
AlgorithmTag = []byte("algorithm")
DerivationPathTag = []byte("derivation_path")
KeyIndexTag = []byte("key_index")
RequestIDTag = []byte("request_id")
EventIDTag = []byte("event_id")
StatusTag = []byte("status")
ErrorTag = []byte("error")
)
// Trust levels for trust acts
type TrustLevel string
const (
TrustLevelHigh TrustLevel = "high"
TrustLevelMedium TrustLevel = "medium"
TrustLevelLow TrustLevel = "low"
)
// Reason types for trust establishment
type TrustReason string
const (
TrustReasonManual TrustReason = "manual"
TrustReasonAutomatic TrustReason = "automatic"
TrustReasonInherited TrustReason = "inherited"
)
// Key purposes for public key advertisements
type KeyPurpose string
const (
KeyPurposeSigning KeyPurpose = "signing"
KeyPurposeEncryption KeyPurpose = "encryption"
KeyPurposeDelegation KeyPurpose = "delegation"
)
// Replication status codes
type ReplicationStatus string
const (
ReplicationStatusSuccess ReplicationStatus = "success"
ReplicationStatusError ReplicationStatus = "error"
ReplicationStatusPending ReplicationStatus = "pending"
)
// GenerateNonce creates a cryptographically secure random nonce for use in
// identity tags and other protocol messages.
func GenerateNonce(size int) (nonce []byte, err error) {
if size <= 0 {
size = 16 // Default to 16 bytes
}
nonce = make([]byte, size)
if _, err = rand.Read(nonce); chk.E(err) {
return
}
return
}
// GenerateNonceHex creates a hex-encoded nonce of the specified byte size.
func GenerateNonceHex(size int) (nonceHex string, err error) {
var nonce []byte
if nonce, err = GenerateNonce(size); chk.E(err) {
return
}
nonceHex = hex.EncodeToString(nonce)
return
}
// IsDirectoryEventKind returns true if the given kind is a directory event
// that should always be replicated among consortium members.
//
// Directory events include:
// - 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
func IsDirectoryEventKind(k uint16) (isDirectory bool) {
switch k {
case 0, 3, 5, 1984, 10002, 10000, 10050:
return true
default:
return false
}
}
// ValidateTrustLevel checks if the provided trust level is valid.
func ValidateTrustLevel(level string) (err error) {
switch TrustLevel(level) {
case TrustLevelHigh, TrustLevelMedium, TrustLevelLow:
return nil
default:
return errorf.E("invalid trust level: %s", level)
}
}
// ValidateKeyPurpose checks if the provided key purpose is valid.
func ValidateKeyPurpose(purpose string) (err error) {
switch KeyPurpose(purpose) {
case KeyPurposeSigning, KeyPurposeEncryption, KeyPurposeDelegation:
return nil
default:
return errorf.E("invalid key purpose: %s", purpose)
}
}
// ValidateReplicationStatus checks if the provided replication status is valid.
func ValidateReplicationStatus(status string) (err error) {
switch ReplicationStatus(status) {
case ReplicationStatusSuccess, ReplicationStatusError, ReplicationStatusPending:
return nil
default:
return errorf.E("invalid replication status: %s", status)
}
}
// CreateBaseEvent creates a basic event structure with common fields set.
func CreateBaseEvent(pubkey []byte, k *kind.K) (ev *event.E) {
return &event.E{
Pubkey: pubkey,
CreatedAt: time.Now().Unix(),
Kind: k.K,
Tags: tag.NewS(),
Content: []byte(""),
}
}

View File

@@ -0,0 +1,359 @@
package directory
import (
"crypto/sha256"
"encoding/hex"
"net/url"
"regexp"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/crypto/ec/schnorr"
"next.orly.dev/pkg/crypto/ec/secp256k1"
"next.orly.dev/pkg/encoders/bech32encoding"
"next.orly.dev/pkg/encoders/event"
)
// Validation constants
const (
MaxKeyDelegations = 512
KeyExpirationDays = 30
MinNonceSize = 16 // bytes
MaxContentLength = 65536 // bytes
)
// Regular expressions for validation
var (
hexKeyRegex = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
npubRegex = regexp.MustCompile(`^npub1[0-9a-z]+$`)
wsURLRegex = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
)
// ValidateHexKey validates that a string is a valid 64-character hex key.
func ValidateHexKey(key string) (err error) {
if !hexKeyRegex.MatchString(key) {
return errorf.E("invalid hex key format: must be 64 hex characters")
}
return nil
}
// ValidateNPub validates that a string is a valid npub-encoded public key.
func ValidateNPub(npub string) (err error) {
if !npubRegex.MatchString(npub) {
return errorf.E("invalid npub format")
}
// Try to decode to verify it's valid
if _, err = bech32encoding.NpubToBytes(npub); chk.E(err) {
return errorf.E("invalid npub encoding: %w", err)
}
return nil
}
// ValidateWebSocketURL validates that a string is a valid WebSocket URL.
func ValidateWebSocketURL(wsURL string) (err error) {
if !wsURLRegex.MatchString(wsURL) {
return errorf.E("invalid WebSocket URL format")
}
// Parse URL for additional validation
var u *url.URL
if u, err = url.Parse(wsURL); chk.E(err) {
return errorf.E("invalid URL: %w", err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return errorf.E("URL must use ws:// or wss:// scheme")
}
if u.Host == "" {
return errorf.E("URL must have a host")
}
return nil
}
// ValidateNonce validates that a nonce meets minimum security requirements.
func ValidateNonce(nonce string) (err error) {
if len(nonce) < MinNonceSize*2 { // hex-encoded, so double the byte length
return errorf.E("nonce must be at least %d bytes (%d hex characters)",
MinNonceSize, MinNonceSize*2)
}
// Verify it's valid hex
if _, err = hex.DecodeString(nonce); chk.E(err) {
return errorf.E("nonce must be valid hex: %w", err)
}
return nil
}
// ValidateSignature validates that a signature is properly formatted.
func ValidateSignature(sig string) (err error) {
if len(sig) != 128 { // 64 bytes hex-encoded
return errorf.E("signature must be 64 bytes (128 hex characters)")
}
// Verify it's valid hex
if _, err = hex.DecodeString(sig); chk.E(err) {
return errorf.E("signature must be valid hex: %w", err)
}
return nil
}
// ValidateDerivationPath validates a BIP32 derivation path for this protocol.
func ValidateDerivationPath(path string) (err error) {
// Expected format: m/39103'/1237'/identity'/usage/index
if !strings.HasPrefix(path, "m/39103'/1237'/") {
return errorf.E("derivation path must start with m/39103'/1237'/")
}
parts := strings.Split(path, "/")
if len(parts) != 6 {
return errorf.E("derivation path must have 6 components")
}
// Validate hardened components
if parts[1] != "39103'" || parts[2] != "1237'" {
return errorf.E("invalid hardened components in derivation path")
}
// Identity component should be hardened (end with ')
if !strings.HasSuffix(parts[3], "'") {
return errorf.E("identity component must be hardened")
}
return nil
}
// ValidateEventContent validates that event content is within size limits.
func ValidateEventContent(content []byte) (err error) {
if len(content) > MaxContentLength {
return errorf.E("content exceeds maximum length of %d bytes", MaxContentLength)
}
return nil
}
// ValidateTimestamp validates that a timestamp is reasonable (not too far in past/future).
func ValidateTimestamp(ts int64) (err error) {
now := time.Now().Unix()
// Allow up to 1 hour in the future
if ts > now+3600 {
return errorf.E("timestamp too far in the future")
}
// Allow up to 1 year in the past
if ts < now-31536000 {
return errorf.E("timestamp too far in the past")
}
return nil
}
// VerifyIdentityTagSignature verifies the signature in an identity tag.
func VerifyIdentityTagSignature(
identityTag *IdentityTag,
delegatePubkey []byte,
) (valid bool, err error) {
if identityTag == nil {
return false, errorf.E("identity tag cannot be nil")
}
// Decode npub to get identity public key
var identityPubkey []byte
if identityPubkey, err = bech32encoding.NpubToBytes(identityTag.NPubIdentity); chk.E(err) {
return false, errorf.E("failed to decode npub: %w", err)
}
// Decode nonce and signature
var nonce, signature []byte
if nonce, err = hex.DecodeString(identityTag.Nonce); chk.E(err) {
return false, errorf.E("invalid nonce hex: %w", err)
}
if signature, err = hex.DecodeString(identityTag.Signature); chk.E(err) {
return false, errorf.E("invalid signature hex: %w", err)
}
// Create message to verify: nonce + delegate_pubkey_hex + identity_pubkey_hex
message := make([]byte, 0, len(nonce)+64+64)
message = append(message, nonce...)
message = append(message, []byte(hex.EncodeToString(delegatePubkey))...)
message = append(message, []byte(hex.EncodeToString(identityPubkey))...)
// Hash the message
hash := sha256.Sum256(message)
// Parse signature and verify
var sig *schnorr.Signature
if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
return false, errorf.E("failed to parse signature: %w", err)
}
// Parse public key
var pubKey *secp256k1.PublicKey
if pubKey, err = schnorr.ParsePubKey(identityPubkey); chk.E(err) {
return false, errorf.E("failed to parse public key: %w", err)
}
return sig.Verify(hash[:], pubKey), nil
}
// ValidateEventKindForReplication validates that an event kind is appropriate
// for replication in the directory consensus protocol.
func ValidateEventKindForReplication(kind uint16) (err error) {
// Directory events are always valid
if IsDirectoryEventKind(kind) {
return nil
}
// Protocol events (39100-39105) should not be replicated as regular events
if kind >= 39100 && kind <= 39105 {
return errorf.E("protocol events should not be replicated as directory events")
}
// Ephemeral events (20000-29999) should not be stored
if kind >= 20000 && kind <= 29999 {
return errorf.E("ephemeral events should not be replicated")
}
return nil
}
// ValidateRelayIdentityBinding verifies that a relay identity announcement
// is properly bound to its network address through NIP-11 signature verification.
func ValidateRelayIdentityBinding(
announcement *RelayIdentityAnnouncement,
nip11Pubkey, nip11Nonce, nip11Sig, relayAddress string,
) (valid bool, err error) {
if announcement == nil {
return false, errorf.E("announcement cannot be nil")
}
// Verify the announcement event pubkey matches the NIP-11 pubkey
announcementPubkeyHex := hex.EncodeToString(announcement.Event.Pubkey)
if announcementPubkeyHex != nip11Pubkey {
return false, errorf.E("announcement pubkey does not match NIP-11 pubkey")
}
// Verify NIP-11 signature format
if err = ValidateHexKey(nip11Pubkey); chk.E(err) {
return false, errorf.E("invalid NIP-11 pubkey: %w", err)
}
if err = ValidateNonce(nip11Nonce); chk.E(err) {
return false, errorf.E("invalid NIP-11 nonce: %w", err)
}
if err = ValidateSignature(nip11Sig); chk.E(err) {
return false, errorf.E("invalid NIP-11 signature: %w", err)
}
// Decode components
var pubkey, signature []byte
if pubkey, err = hex.DecodeString(nip11Pubkey); chk.E(err) {
return false, errorf.E("failed to decode NIP-11 pubkey: %w", err)
}
if signature, err = hex.DecodeString(nip11Sig); chk.E(err) {
return false, errorf.E("failed to decode NIP-11 signature: %w", err)
}
// Create message: pubkey + nonce + relay_address
message := nip11Pubkey + nip11Nonce + relayAddress
hash := sha256.Sum256([]byte(message))
// Parse signature and verify
var sig *schnorr.Signature
if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
return false, errorf.E("failed to parse signature: %w", err)
}
// Parse public key
var pubKey *secp256k1.PublicKey
if pubKey, err = schnorr.ParsePubKey(pubkey); chk.E(err) {
return false, errorf.E("failed to parse public key: %w", err)
}
return sig.Verify(hash[:], pubKey), nil
}
// ValidateConsortiumEvent performs comprehensive validation of any consortium
// protocol event, including signature verification and protocol-specific checks.
func ValidateConsortiumEvent(ev *event.E) (err error) {
if ev == nil {
return errorf.E("event cannot be nil")
}
// Verify basic event signature
if _, err = ev.Verify(); chk.E(err) {
return errorf.E("invalid event signature: %w", err)
}
// Validate timestamp
if err = ValidateTimestamp(ev.CreatedAt); chk.E(err) {
return errorf.E("invalid timestamp: %w", err)
}
// Validate content size
if err = ValidateEventContent(ev.Content); chk.E(err) {
return errorf.E("invalid content: %w", err)
}
// Protocol-specific validation based on event kind
switch ev.Kind {
case RelayIdentityAnnouncementKind.K:
var ria *RelayIdentityAnnouncement
if ria, err = ParseRelayIdentityAnnouncement(ev); chk.E(err) {
return errorf.E("failed to parse relay identity announcement: %w", err)
}
return ria.Validate()
case TrustActKind.K:
var ta *TrustAct
if ta, err = ParseTrustAct(ev); chk.E(err) {
return errorf.E("failed to parse trust act: %w", err)
}
return ta.Validate()
case GroupTagActKind.K:
var gta *GroupTagAct
if gta, err = ParseGroupTagAct(ev); chk.E(err) {
return errorf.E("failed to parse group tag act: %w", err)
}
return gta.Validate()
case PublicKeyAdvertisementKind.K:
var pka *PublicKeyAdvertisement
if pka, err = ParsePublicKeyAdvertisement(ev); chk.E(err) {
return errorf.E("failed to parse public key advertisement: %w", err)
}
return pka.Validate()
case DirectoryEventReplicationRequestKind.K:
var derr *DirectoryEventReplicationRequest
if derr, err = ParseDirectoryEventReplicationRequest(ev); chk.E(err) {
return errorf.E("failed to parse replication request: %w", err)
}
return derr.Validate()
case DirectoryEventReplicationResponseKind.K:
var derr *DirectoryEventReplicationResponse
if derr, err = ParseDirectoryEventReplicationResponse(ev); chk.E(err) {
return errorf.E("failed to parse replication response: %w", err)
}
return derr.Validate()
default:
return errorf.E("unknown consortium event kind: %d", ev.Kind)
}
}
// IsConsortiumEvent returns true if the event is a consortium protocol event.
func IsConsortiumEvent(ev *event.E) bool {
if ev == nil {
return false
}
return ev.Kind >= 39100 && ev.Kind <= 39105
}