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