A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at test-validate 174 lines 4.9 kB view raw
1package bundle 2 3import ( 4 "encoding/json" 5 "fmt" 6 "time" 7 8 didplc "github.com/did-method-plc/go-didplc" 9 "tangled.org/atscan.net/plcbundle/plc" 10) 11 12// Validator validates PLC operations using go-didplc 13type Validator struct { 14 logger Logger 15} 16 17// InvalidOperation represents an operation that failed validation 18type InvalidOperation struct { 19 CID string 20 DID string 21 Reason string 22} 23 24// InvalidCallback is called immediately when an invalid operation is found 25type InvalidCallback func(InvalidOperation) 26 27// NewValidator creates a new validator 28func NewValidator(logger Logger) *Validator { 29 return &Validator{ 30 logger: logger, 31 } 32} 33 34// ValidateBundleOperations validates all operations (simple version, returns error only) 35func (v *Validator) ValidateBundleOperations(ops []plc.PLCOperation) error { 36 _, err := v.ValidateBundleOperationsWithDetails(ops) 37 return err 38} 39 40// ValidateBundleOperationsWithDetails validates and returns details about invalid operations 41func (v *Validator) ValidateBundleOperationsWithDetails(ops []plc.PLCOperation) ([]InvalidOperation, error) { 42 var invalid []InvalidOperation 43 44 // Use streaming validation but collect results 45 err := v.ValidateBundleOperationsStreaming(ops, func(inv InvalidOperation) { 46 invalid = append(invalid, inv) 47 }) 48 49 return invalid, err 50} 51 52// ValidateBundleOperationsStreaming validates and streams invalid operations via callback 53func (v *Validator) ValidateBundleOperationsStreaming(ops []plc.PLCOperation, callback InvalidCallback) error { 54 if len(ops) == 0 { 55 return nil 56 } 57 58 // First pass: validate each operation individually and parse 59 opsByDID := make(map[string][]didplc.LogEntry) 60 opCIDMap := make(map[string]string) // CID -> DID mapping 61 parseErrors := 0 62 validationErrors := 0 63 64 for _, op := range ops { 65 opCIDMap[op.CID] = op.DID 66 67 // Try to parse operation 68 var opEnum didplc.OpEnum 69 if err := parseOperationToEnum(op, &opEnum); err != nil { 70 if callback != nil { 71 callback(InvalidOperation{ 72 CID: op.CID, 73 DID: op.DID, 74 Reason: fmt.Sprintf("parse error: %v", err), 75 }) 76 } 77 parseErrors++ 78 continue 79 } 80 81 // Create log entry 82 logEntry := didplc.LogEntry{ 83 DID: op.DID, 84 CID: op.CID, 85 CreatedAt: op.CreatedAt.Format(time.RFC3339Nano), 86 Nullified: op.IsNullified(), 87 Operation: opEnum, 88 } 89 90 // Validate individual entry (checks CID match, signature for genesis, etc.) 91 if err := logEntry.Validate(); err != nil { 92 if callback != nil { 93 callback(InvalidOperation{ 94 CID: op.CID, 95 DID: op.DID, 96 Reason: fmt.Sprintf("validation error: %v", err), 97 }) 98 } 99 validationErrors++ 100 // Still add to chain for chain validation (some errors might be at chain level) 101 } 102 103 opsByDID[op.DID] = append(opsByDID[op.DID], logEntry) 104 } 105 106 // Second pass: validate chains (chronological order, nullification, etc.) 107 chainErrors := 0 108 for did, entries := range opsByDID { 109 if err := didplc.VerifyOpLog(entries); err != nil { 110 // Chain validation failed - report which specific operations are affected 111 // Try to be more specific about which operations caused the failure 112 errMsg := err.Error() 113 114 if callback != nil { 115 // For chain errors, report all operations in the chain 116 // (we don't know which specific one caused it without more detailed analysis) 117 for _, entry := range entries { 118 callback(InvalidOperation{ 119 CID: entry.CID, 120 DID: did, 121 Reason: fmt.Sprintf("chain error: %v", errMsg), 122 }) 123 } 124 } 125 chainErrors++ 126 } 127 } 128 129 totalErrors := parseErrors + validationErrors + chainErrors 130 if totalErrors > 0 { 131 return fmt.Errorf("%d parse errors, %d validation errors, %d chain errors", 132 parseErrors, validationErrors, chainErrors) 133 } 134 135 return nil 136} 137 138// parseOperationToEnum converts plc.PLCOperation to didplc.OpEnum 139func parseOperationToEnum(op plc.PLCOperation, opEnum *didplc.OpEnum) error { 140 // Try to use RawJSON first for exact parsing 141 if len(op.RawJSON) > 0 { 142 // Extract just the operation part from the full record 143 var fullRecord map[string]interface{} 144 if err := json.Unmarshal(op.RawJSON, &fullRecord); err != nil { 145 return fmt.Errorf("failed to unmarshal RawJSON: %w", err) 146 } 147 148 // Get the "operation" field 149 if opData, ok := fullRecord["operation"]; ok { 150 // Re-marshal just the operation data 151 data, err := json.Marshal(opData) 152 if err != nil { 153 return fmt.Errorf("failed to marshal operation: %w", err) 154 } 155 156 if err := json.Unmarshal(data, opEnum); err != nil { 157 return fmt.Errorf("failed to unmarshal into OpEnum: %w", err) 158 } 159 return nil 160 } 161 } 162 163 // Fallback: use the Operation map 164 data, err := json.Marshal(op.Operation) 165 if err != nil { 166 return fmt.Errorf("failed to marshal operation: %w", err) 167 } 168 169 if err := json.Unmarshal(data, opEnum); err != nil { 170 return fmt.Errorf("failed to unmarshal into OpEnum: %w", err) 171 } 172 173 return nil 174}