Browse Source
- 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.main
14 changed files with 3287 additions and 44 deletions
@ -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 |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 != "" |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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(""), |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
Loading…
Reference in new issue