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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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