8 Commits

Author SHA1 Message Date
jay 29ba275293 add individual value validator functions. 2026-04-22 14:20:57 -04:00
jay 747781f5bf Performant validation. Prevent redundant decoding. Remove unused errors. 2026-04-20 23:52:50 -04:00
jay 62aeef4eaf Performant event serialization. Update README. 2026-04-20 23:52:40 -04:00
jay b545f9370f Add bump script. 2026-02-25 13:04:01 -05:00
jay 8c7113c51b Update c2p script. 2026-02-06 10:47:06 -05:00
jay cda73bf6f2 Updated c2p script 2025-11-02 16:27:23 -05:00
jay 1e2a6f7777 Add license file. 2025-11-02 14:27:01 -05:00
jay d42d877ea2 Updated README. 2025-11-02 14:22:45 -05:00
9 changed files with 179 additions and 83 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 nostr:npub10mtatsat7ph6rsq0w8u8npt8d86x4jfr2nqjnvld2439q6f8ugqq0x27hf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+4 -13
View File
@@ -6,7 +6,7 @@ Mirror: https://github.com/wisehodl/go-roots
## What this library does
`go-roots` is a purposefully minimal Nostr protocol library for golang.
`go-roots` is a consensus-layer Nostr protocol library for golang.
It only provides primitives that define protocol compliance:
- Event Structure
@@ -96,10 +96,7 @@ event := events.Event{
}
// 2. Compute the event ID
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
id := events.GetID(event)
event.ID = id
// 3. Sign the event
@@ -114,19 +111,13 @@ event.Sig = sig
```go
// Returns canonical JSON: [0, pubkey, created_at, kind, tags, content]
serialized, err := events.Serialize(event)
if err != nil {
log.Fatal(err)
}
serialized := events.Serialize(event)
```
#### Compute event ID manually
```go
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
id := events.GetID(event)
// Returns lowercase hex SHA-256 hash of serialized form
```
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
IFS='.' read -r major minor patch <<< "${latest#v}"
case ${1:-patch} in
major) new="v$((major+1)).0.0" ;;
minor) new="v${major}.$((minor+1)).0" ;;
patch) new="v${major}.${minor}.$((patch+1))" ;;
*) echo "Usage: bump.sh [major|minor|patch]" >&2; exit 1 ;;
esac
git tag -a "$new"
+1 -1
View File
@@ -1 +1 @@
code2prompt -e "go.sum" -e "README.md" -e "c2p" .
code2prompt -c -e "go.sum" -e "c2p"
-6
View File
@@ -20,12 +20,6 @@ var (
// MalformedTag indicates an event tag contains fewer than two elements.
MalformedTag = errors.New("tags must contain at least two elements")
// FailedIDComp indicates the event ID could not be computed during validation.
FailedIDComp = errors.New("failed to compute event id")
// 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")
)
-14
View File
@@ -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}$")
)
+76 -27
View File
@@ -3,35 +3,84 @@ package events
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"strconv"
)
// 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, error) {
serialized := []interface{}{
0,
e.PubKey,
e.CreatedAt,
e.Kind,
e.Tags,
e.Content,
}
bytes, err := json.Marshal(serialized)
if err != nil {
return []byte{}, err
}
return bytes, nil
}
// GetID computes and returns the event ID as a lowercase, hex-encoded SHA-256 hash
// of the serialized event.
func GetID(e Event) (string, error) {
bytes, err := Serialize(e)
if err != nil {
return "", err
}
hash := sha256.Sum256(bytes)
return hex.EncodeToString(hash[:]), nil
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 {
buf := make([]byte, 0, 100+len(e.Content)+len(e.Tags)*80)
return appendSerialized(buf, e)
}
func appendSerialized(dst []byte, e Event) []byte {
dst = append(dst, "[0,\""...)
dst = append(dst, e.PubKey...)
dst = append(dst, "\","...)
dst = strconv.AppendInt(dst, int64(e.CreatedAt), 10)
dst = append(dst, ',')
dst = strconv.AppendInt(dst, int64(e.Kind), 10)
dst = append(dst, ",["...)
for i, tag := range e.Tags {
if i > 0 {
dst = append(dst, ',')
}
dst = append(dst, '[')
for j, s := range tag {
if j > 0 {
dst = append(dst, ',')
}
dst = appendEscapedString(dst, s)
}
dst = append(dst, ']')
}
dst = append(dst, "],"...)
dst = appendEscapedString(dst, e.Content)
return append(dst, ']')
}
func appendEscapedString(dst []byte, s string) []byte {
dst = append(dst, '"')
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c == '"':
dst = append(dst, '\\', '"')
case c == '\\':
dst = append(dst, '\\', '\\')
case c >= 0x20:
dst = append(dst, c)
case c == 0x08:
dst = append(dst, '\\', 'b')
case c == 0x09:
dst = append(dst, '\\', 't')
case c == 0x0a:
dst = append(dst, '\\', 'n')
case c == 0x0c:
dst = append(dst, '\\', 'f')
case c == 0x0d:
dst = append(dst, '\\', 'r')
case c < 0x09:
dst = append(dst, '\\', 'u', '0', '0', '0', '0'+c)
case c < 0x10:
dst = append(dst, '\\', 'u', '0', '0', '0', 0x57+c)
case c < 0x1a:
dst = append(dst, '\\', 'u', '0', '0', '1', 0x20+c)
case c < 0x20:
dst = append(dst, '\\', 'u', '0', '0', '1', 0x47+c)
}
}
return append(dst, '"')
}
+1 -2
View File
@@ -196,8 +196,7 @@ var idTestCases = []IDTestCase{
func TestEventGetId(t *testing.T) {
for _, tc := range idTestCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := GetID(tc.event)
assert.NoError(t, err)
actual := GetID(tc.event)
assert.Equal(t, tc.expected, actual)
})
}
+62 -20
View File
@@ -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 !IsValidKey(e.PubKey) {
return errors.MalformedPubKey
}
if !Hex64Pattern.MatchString(e.ID) {
if !IsValidID(e.ID) {
return errors.MalformedID
}
if !Hex128Pattern.MatchString(e.Sig) {
if !IsValidSig(e.Sig) {
return errors.MalformedSig
}
@@ -47,17 +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, err := GetID(e)
if err != nil {
return errors.FailedIDComp
}
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
@@ -67,13 +60,49 @@ 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)
// Value validators
// IsValidKey verifies that a public or private key is properly formatted.
func IsValidKey(value string) bool {
return isLowerHex(value, 64)
}
// IsValidKey verifies that an event id is properly formatted.
func IsValidID(value string) bool {
return isLowerHex(value, 64)
}
// IsValidKey verifies that an event signature is properly formatted.
func IsValidSig(value string) bool {
return isLowerHex(value, 128)
}
// 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)
}
@@ -90,7 +119,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
}