From 747781f5bf6f38c72dbc15681ee9b8ce0cad9a1b Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 20 Apr 2026 22:59:48 -0400 Subject: [PATCH] Performant validation. Prevent redundant decoding. Remove unused errors. --- errors/errors.go | 3 --- events/event.go | 14 ----------- events/id.go | 20 +++++++++------ events/validate.go | 62 +++++++++++++++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 34972a1..7b2b322 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -20,9 +20,6 @@ var ( // MalformedTag indicates an event tag contains fewer than two elements. MalformedTag = errors.New("tags must contain at least two elements") - // NoEventID indicates the event ID field is empty. - NoEventID = errors.New("event id is empty") - // InvalidSig indicates the event signature failed cryptographic validation. InvalidSig = errors.New("event signature is invalid") ) diff --git a/events/event.go b/events/event.go index 245f33d..bae500d 100644 --- a/events/event.go +++ b/events/event.go @@ -3,10 +3,6 @@ // serialization, cryptographic signatures, and subscription filters. package events -import ( - "regexp" -) - // Tag represents a single tag within an event as an array of strings. // The first element identifies the tag name, the second contains the value, // and subsequent elements are optional. @@ -23,13 +19,3 @@ type Event struct { Content string `json:"content"` Sig string `json:"sig"` } - -var ( - // Hex64Pattern matches 64-character, lowercase, hexadecimal strings. - // Used for validating event IDs and cryptographic keys. - Hex64Pattern = regexp.MustCompile("^[a-f0-9]{64}$") - - // Hex128Pattern matches 128-character, lowercase, hexadecimal strings. - // Used for validating signatures. - Hex128Pattern = regexp.MustCompile("^[a-f0-9]{128}$") -) diff --git a/events/id.go b/events/id.go index a01d319..3f8399e 100644 --- a/events/id.go +++ b/events/id.go @@ -6,6 +6,18 @@ import ( "strconv" ) +// GetID computes and returns the event ID as a lowercase, hex-encoded SHA-256 hash +// of the serialized event. +func GetID(e Event) string { + hash := GetIDBytes(e) + return hex.EncodeToString(hash[:]) +} + +// GetIDBytes computes and returns the event ID as a raw SHA256 digest +func GetIDBytes(e Event) [32]byte { + return sha256.Sum256(Serialize(e)) +} + // Serialize returns the canonical JSON array representation of the event. // used for ID computation: [0, pubkey, created_at, kind, tags, content]. func Serialize(e Event) []byte { @@ -13,14 +25,6 @@ func Serialize(e Event) []byte { return appendSerialized(buf, e) } -// GetID computes and returns the event ID as a lowercase, hex-encoded SHA-256 hash -// of the serialized event. -func GetID(e Event) string { - bytes := Serialize(e) - hash := sha256.Sum256(bytes) - return hex.EncodeToString(hash[:]) -} - func appendSerialized(dst []byte, e Event) []byte { dst = append(dst, "[0,\""...) dst = append(dst, e.PubKey...) diff --git a/events/validate.go b/events/validate.go index accf41b..894c4a4 100644 --- a/events/validate.go +++ b/events/validate.go @@ -1,6 +1,7 @@ package events import ( + "bytes" "encoding/hex" "fmt" "git.wisehodl.dev/jay/go-roots/errors" @@ -14,25 +15,26 @@ func Validate(e Event) error { return err } - if err := ValidateID(e); err != nil { + idBytes, err := checkIDMatch(e) + if err != nil { return err } - return ValidateSignature(e) + return validateSignatureBytes(idBytes, e.Sig, e.PubKey) } // ValidateStructure checks that all event fields conform to the protocol // specification: hex lengths, tag structure, and field formats. func ValidateStructure(e Event) error { - if !Hex64Pattern.MatchString(e.PubKey) { + if !isLowerHex(e.PubKey, 64) { return errors.MalformedPubKey } - if !Hex64Pattern.MatchString(e.ID) { + if !isLowerHex(e.ID, 64) { return errors.MalformedID } - if !Hex128Pattern.MatchString(e.Sig) { + if !isLowerHex(e.Sig, 128) { return errors.MalformedSig } @@ -47,14 +49,8 @@ func ValidateStructure(e Event) error { // ValidateID recomputes the event ID and verifies it matches the stored ID field. func ValidateID(e Event) error { - computedID := GetID(e) - if e.ID == "" { - return errors.NoEventID - } - if computedID != e.ID { - return fmt.Errorf("event id %q does not match computed id %q", e.ID, computedID) - } - return nil + _, err := checkIDMatch(e) + return err } // ValidateSignature verifies the event signature is cryptographically valid @@ -64,13 +60,32 @@ func ValidateSignature(e Event) error { if err != nil { return fmt.Errorf("invalid event id hex: %w", err) } + return validateSignatureBytes(idBytes, e.Sig, e.PubKey) +} - sigBytes, err := hex.DecodeString(e.Sig) +// Helpers + +func checkIDMatch(e Event) ([]byte, error) { + idHash := GetIDBytes(e) + idBytes, err := hex.DecodeString(e.ID) + if err != nil { + return nil, errors.MalformedID + } + if !bytes.Equal(idBytes, idHash[:]) { + return nil, fmt.Errorf( + "event id %q does not match computed id %q", + e.ID, hex.EncodeToString(idHash[:])) + } + return idBytes, nil +} + +func validateSignatureBytes(idBytes []byte, sigHex, pkHex string) error { + sigBytes, err := hex.DecodeString(sigHex) if err != nil { return fmt.Errorf("invalid event signature hex: %w", err) } - pkBytes, err := hex.DecodeString(e.PubKey) + pkBytes, err := hex.DecodeString(pkHex) if err != nil { return fmt.Errorf("invalid public key hex: %w", err) } @@ -87,7 +102,20 @@ func ValidateSignature(e Event) error { if signature.Verify(idBytes, publicKey) { return nil - } else { - return errors.InvalidSig } + + return errors.InvalidSig +} + +func isLowerHex(s string, n int) bool { + if len(s) != n { + return false + } + for i := 0; i < n; i++ { + c := s[i] + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { + return false + } + } + return true }