Something went wrong. Try again.
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175package bundle
import ( "encoding/json" "fmt" "time"
didplc "github.com/did-method-plc/go-didplc" "tangled.org/atscan.net/plcbundle/plc")
// Validator validates PLC operations using go-didplctype Validator struct { logger Logger}
// InvalidOperation represents an operation that failed validationtype InvalidOperation struct { CID string DID string Reason string}
// InvalidCallback is called immediately when an invalid operation is foundtype InvalidCallback func(InvalidOperation)
// NewValidator creates a new validatorfunc NewValidator(logger Logger) *Validator { return &Validator{ logger: logger, }}
// ValidateBundleOperations validates all operations (simple version, returns error only)func (v *Validator) ValidateBundleOperations(ops []plc.PLCOperation) error { _, err := v.ValidateBundleOperationsWithDetails(ops) return err}
// ValidateBundleOperationsWithDetails validates and returns details about invalid operationsfunc (v *Validator) ValidateBundleOperationsWithDetails(ops []plc.PLCOperation) ([]InvalidOperation, error) { var invalid []InvalidOperation
// Use streaming validation but collect results err := v.ValidateBundleOperationsStreaming(ops, func(inv InvalidOperation) { invalid = append(invalid, inv) })
return invalid, err}
// ValidateBundleOperationsStreaming validates and streams invalid operations via callbackfunc (v *Validator) ValidateBundleOperationsStreaming(ops []plc.PLCOperation, callback InvalidCallback) error { if len(ops) == 0 { return nil }
// First pass: validate each operation individually and parse opsByDID := make(map[string][]didplc.LogEntry) opCIDMap := make(map[string]string) // CID -> DID mapping parseErrors := 0 validationErrors := 0
for _, op := range ops { opCIDMap[op.CID] = op.DID
// Try to parse operation var opEnum didplc.OpEnum if err := parseOperationToEnum(op, &opEnum); err != nil { if callback != nil { callback(InvalidOperation{ CID: op.CID, DID: op.DID, Reason: fmt.Sprintf("parse error: %v", err), }) } parseErrors++ continue }
// Create log entry logEntry := didplc.LogEntry{ DID: op.DID, CID: op.CID, CreatedAt: op.CreatedAt.Format(time.RFC3339Nano), Nullified: op.IsNullified(), Operation: opEnum, }
// Validate individual entry (checks CID match, signature for genesis, etc.) if err := logEntry.Validate(); err != nil { if callback != nil { callback(InvalidOperation{ CID: op.CID, DID: op.DID, Reason: fmt.Sprintf("validation error: %v", err), }) } validationErrors++ // Still add to chain for chain validation (some errors might be at chain level) }
opsByDID[op.DID] = append(opsByDID[op.DID], logEntry) }
// Second pass: validate chains (chronological order, nullification, etc.) chainErrors := 0 for did, entries := range opsByDID { if err := didplc.VerifyOpLog(entries); err != nil { // Chain validation failed - report which specific operations are affected // Try to be more specific about which operations caused the failure errMsg := err.Error()
if callback != nil { // For chain errors, report all operations in the chain // (we don't know which specific one caused it without more detailed analysis) for _, entry := range entries { callback(InvalidOperation{ CID: entry.CID, DID: did, Reason: fmt.Sprintf("chain error: %v", errMsg), }) } } chainErrors++ } }
totalErrors := parseErrors + validationErrors + chainErrors if totalErrors > 0 { return fmt.Errorf("%d parse errors, %d validation errors, %d chain errors", parseErrors, validationErrors, chainErrors) }
return nil}
// parseOperationToEnum converts plc.PLCOperation to didplc.OpEnumfunc parseOperationToEnum(op plc.PLCOperation, opEnum *didplc.OpEnum) error { // Try to use RawJSON first for exact parsing if len(op.RawJSON) > 0 { // Extract just the operation part from the full record var fullRecord map[string]interface{} if err := json.Unmarshal(op.RawJSON, &fullRecord); err != nil { return fmt.Errorf("failed to unmarshal RawJSON: %w", err) }
// Get the "operation" field if opData, ok := fullRecord["operation"]; ok { // Re-marshal just the operation data data, err := json.Marshal(opData) if err != nil { return fmt.Errorf("failed to marshal operation: %w", err) }
if err := json.Unmarshal(data, opEnum); err != nil { return fmt.Errorf("failed to unmarshal into OpEnum: %w", err) } return nil } }
// Fallback: use the Operation map data, err := json.Marshal(op.Operation) if err != nil { return fmt.Errorf("failed to marshal operation: %w", err) }
if err := json.Unmarshal(data, opEnum); err != nil { return fmt.Errorf("failed to unmarshal into OpEnum: %w", err) }
return nil}