Refactor NIP-XX Document and Protocol Implementation for Directory Consensus

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

View File

@@ -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 != ""
}