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:
@@ -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) {
|
||||||
|
|||||||
@@ -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
2
go.mod
@@ -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
17
go.sum
@@ -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=
|
||||||
|
|||||||
376
pkg/protocol/directory/doc.go
Normal file
376
pkg/protocol/directory/doc.go
Normal 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
|
||||||
241
pkg/protocol/directory/group_tag_act.go
Normal file
241
pkg/protocol/directory/group_tag_act.go
Normal 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
|
||||||
|
}
|
||||||
426
pkg/protocol/directory/helpers.go
Normal file
426
pkg/protocol/directory/helpers.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
368
pkg/protocol/directory/public_key_advertisement.go
Normal file
368
pkg/protocol/directory/public_key_advertisement.go
Normal 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
|
||||||
|
}
|
||||||
264
pkg/protocol/directory/relay_identity.go
Normal file
264
pkg/protocol/directory/relay_identity.go
Normal 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
|
||||||
|
}
|
||||||
278
pkg/protocol/directory/replication_request.go
Normal file
278
pkg/protocol/directory/replication_request.go
Normal 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
|
||||||
|
}
|
||||||
367
pkg/protocol/directory/replication_response.go
Normal file
367
pkg/protocol/directory/replication_response.go
Normal 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 != ""
|
||||||
|
}
|
||||||
378
pkg/protocol/directory/trust_act.go
Normal file
378
pkg/protocol/directory/trust_act.go
Normal 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
|
||||||
|
}
|
||||||
205
pkg/protocol/directory/types.go
Normal file
205
pkg/protocol/directory/types.go
Normal 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(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
359
pkg/protocol/directory/validation.go
Normal file
359
pkg/protocol/directory/validation.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user