3 Commits

Author SHA1 Message Date
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
Jay
4df91938ef Refactored methods into pure functions. 2025-10-31 19:48:56 -04:00
10 changed files with 64 additions and 43 deletions

21
LICENSE Normal file
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.

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,7 +96,7 @@ event := events.Event{
}
// 2. Compute the event ID
id, err := event.GetID()
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
@@ -114,7 +114,7 @@ event.Sig = sig
```go
// Returns canonical JSON: [0, pubkey, created_at, kind, tags, content]
serialized, err := event.Serialize()
serialized, err := events.Serialize(event)
if err != nil {
log.Fatal(err)
}
@@ -123,7 +123,7 @@ if err != nil {
#### Compute event ID manually
```go
id, err := event.GetID()
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
@@ -138,7 +138,7 @@ if err != nil {
```go
// Checks structure, ID computation, and signature
if err := event.Validate(); err != nil {
if err := events.Validate(event); err != nil {
log.Printf("Invalid event: %v", err)
}
```
@@ -147,17 +147,17 @@ if err := event.Validate(); err != nil {
```go
// Check field formats and lengths
if err := event.ValidateStructure(); err != nil {
if err := events.ValidateStructure(event); err != nil {
log.Printf("Malformed structure: %v", err)
}
// Verify ID matches computed hash
if err := event.ValidateID(); err != nil {
if err := events.ValidateID(event); err != nil {
log.Printf("ID mismatch: %v", err)
}
// Verify cryptographic signature
if err := event.ValidateSignature(); err != nil {
if err := events.ValidateSignature(event); err != nil {
log.Printf("Invalid signature: %v", err)
}
```
@@ -186,7 +186,7 @@ if err != nil {
}
// Validate after unmarshaling
if err := event.Validate(); err != nil {
if err := events.Validate(event); err != nil {
log.Printf("Received invalid event: %v", err)
}
```
@@ -250,7 +250,7 @@ filter := filters.Filter{
Kinds: []int{1},
}
if filter.Matches(&event) {
if filters.Matches(filter, event) {
// Event satisfies all filter conditions
}
```
@@ -269,7 +269,7 @@ filter := filters.Filter{
var matches []events.Event
for _, event := range events {
if filter.Matches(&event) {
if filters.Matches(filter, event) {
matches = append(matches, event)
}
}
@@ -293,7 +293,7 @@ filter := filters.Filter{
},
}
jsonBytes, err := filter.MarshalJSON()
jsonBytes, err := filters.MarshalJSON(filter)
// Result: {"ids":["abc123"],"kinds":[1],"#e":["event-id"],"search":"nostr"}
```
@@ -309,7 +309,7 @@ jsonData := `{
}`
var filter filters.Filter
err := filter.UnmarshalJSON([]byte(jsonData))
err := filters.UnmarshalJSON([]byte(jsonData), &filter)
if err != nil {
log.Fatal(err)
}

View File

@@ -9,7 +9,7 @@ import (
func TestUnmarshalEventJSON(t *testing.T) {
event := Event{}
json.Unmarshal(testEventJSONBytes, &event)
if err := event.Validate(); err != nil {
if err := Validate(event); err != nil {
t.Error("unmarshalled event is invalid")
}
expectEqualEvents(t, event, testEvent)
@@ -37,7 +37,7 @@ func TestEventJSONRoundTrip(t *testing.T) {
}
expectedJSON := `{"id":"86e856d0527dd08527498cd8afd8a7d296bde37e4757a8921f034f0b344df3ad","pubkey":"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef","created_at":1760740551,"kind":1,"tags":[["a","value"],["b","value","optional"],["name","value","optional","optional"]],"content":"hello world","sig":"c05fe02a9c082ff56aad2b16b5347498a21665f02f050ba086dbe6bd593c8cd448505d2831d1c0340acc1793eaf89b7c0cb21bb696c71da6b8d6b857702bb557"}`
if err := event.Validate(); err != nil {
if err := Validate(event); err != nil {
t.Error("test event is invalid")
}
eventJSON, err := json.Marshal(event)
@@ -47,7 +47,7 @@ func TestEventJSONRoundTrip(t *testing.T) {
unmarshalledEvent := Event{}
json.Unmarshal(eventJSON, &unmarshalledEvent)
if err := unmarshalledEvent.Validate(); err != nil {
if err := Validate(unmarshalledEvent); err != nil {
t.Error("unmarshalled event is invalid")
}
expectEqualEvents(t, unmarshalledEvent, event)

View File

@@ -8,7 +8,7 @@ import (
// Serialize returns the canonical JSON array representation of the event.
// used for ID computation: [0, pubkey, created_at, kind, tags, content].
func (e *Event) Serialize() ([]byte, error) {
func Serialize(e Event) ([]byte, error) {
serialized := []interface{}{
0,
e.PubKey,
@@ -27,8 +27,8 @@ func (e *Event) Serialize() ([]byte, error) {
// GetID computes and returns the event ID as a lowercase, hex-encoded SHA-256 hash
// of the serialized event.
func (e *Event) GetID() (string, error) {
bytes, err := e.Serialize()
func GetID(e Event) (string, error) {
bytes, err := Serialize(e)
if err != nil {
return "", err
}

View File

@@ -196,7 +196,7 @@ var idTestCases = []IDTestCase{
func TestEventGetId(t *testing.T) {
for _, tc := range idTestCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := tc.event.GetID()
actual, err := GetID(tc.event)
assert.NoError(t, err)
assert.Equal(t, tc.expected, actual)
})

View File

@@ -9,21 +9,21 @@ import (
// Validate performs a complete event validation: structure, ID computation,
// and signature verification. Returns the first error encountered.
func (e *Event) Validate() error {
if err := e.ValidateStructure(); err != nil {
func Validate(e Event) error {
if err := ValidateStructure(e); err != nil {
return err
}
if err := e.ValidateID(); err != nil {
if err := ValidateID(e); err != nil {
return err
}
return e.ValidateSignature()
return ValidateSignature(e)
}
// ValidateStructure checks that all event fields conform to the protocol
// specification: hex lengths, tag structure, and field formats.
func (e *Event) ValidateStructure() error {
func ValidateStructure(e Event) error {
if !Hex64Pattern.MatchString(e.PubKey) {
return errors.MalformedPubKey
}
@@ -46,8 +46,8 @@ func (e *Event) ValidateStructure() error {
}
// ValidateID recomputes the event ID and verifies it matches the stored ID field.
func (e *Event) ValidateID() error {
computedID, err := e.GetID()
func ValidateID(e Event) error {
computedID, err := GetID(e)
if err != nil {
return errors.FailedIDComp
}
@@ -62,7 +62,7 @@ func (e *Event) ValidateID() error {
// ValidateSignature verifies the event signature is cryptographically valid
// for the event ID and public key using Schnorr verification.
func (e *Event) ValidateSignature() error {
func ValidateSignature(e Event) error {
idBytes, err := hex.DecodeString(e.ID)
if err != nil {
return fmt.Errorf("invalid event id hex: %w", err)

View File

@@ -184,7 +184,7 @@ var structureTestCases = []ValidateEventTestCase{
func TestValidateEventStructure(t *testing.T) {
for _, tc := range structureTestCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.event.ValidateStructure()
err := ValidateStructure(tc.event)
assert.ErrorContains(t, err, tc.expectedError)
})
}
@@ -201,7 +201,7 @@ func TestValidateEventIDFailure(t *testing.T) {
Sig: testEvent.Sig,
}
err := event.ValidateID()
err := ValidateID(event)
assert.ErrorContains(t, err, "does not match computed id")
}
@@ -211,7 +211,7 @@ func TestValidateSignature(t *testing.T) {
PubKey: testEvent.PubKey,
Sig: testEvent.Sig,
}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.NoError(t, err)
}
@@ -222,7 +222,7 @@ func TestValidateInvalidSignature(t *testing.T) {
PubKey: testEvent.PubKey,
Sig: "9e43cbcf7e828a21c53fa35371ee79bffbfd7a3063ae46fc05ec623dd3186667c57e3d006488015e19247df35eb41c61013e051aa87860e23fa5ffbd44120482",
}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.ErrorContains(t, err, "event signature is invalid")
}
@@ -281,7 +281,7 @@ func TestValidateSignatureInvalidEventSignature(t *testing.T) {
for _, tc := range validateSignatureTestCases {
t.Run(tc.name, func(t *testing.T) {
event := Event{ID: tc.id, PubKey: tc.pubkey, Sig: tc.sig}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.ErrorContains(t, err, tc.expectedError)
})
}
@@ -301,6 +301,6 @@ func TestValidateEvent(t *testing.T) {
Sig: "668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14",
}
err := event.Validate()
err := Validate(event)
assert.NoError(t, err)
}

View File

@@ -29,7 +29,7 @@ type Filter struct {
// MarshalJSON converts the filter to JSON with standard fields, tag filters
// (prefixed with "#"), and extensions merged into a single object.
func (f *Filter) MarshalJSON() ([]byte, error) {
func MarshalJSON(f Filter) ([]byte, error) {
outputMap := make(map[string]interface{})
// Add standard fields
@@ -86,7 +86,7 @@ func (f *Filter) MarshalJSON() ([]byte, error) {
// UnmarshalJSON parses JSON into the filter, separating standard fields,
// tag filters (keys starting with "#"), and extensions.
func (f *Filter) UnmarshalJSON(data []byte) error {
func UnmarshalJSON(data []byte, f *Filter) error {
// Decode into raw map
raw := make(FilterExtensions)
if err := json.Unmarshal(data, &raw); err != nil {
@@ -182,7 +182,7 @@ func (f *Filter) UnmarshalJSON(data []byte) error {
// Matches returns true if the event satisfies all filter conditions.
// Supports prefix matching for IDs and authors, and tag filtering.
// Does not account for custom extensions.
func (f *Filter) Matches(event *events.Event) bool {
func Matches(f Filter, event events.Event) bool {
// Check ID
if len(f.IDs) > 0 {
if !matchesPrefix(event.ID, f.IDs) {

View File

@@ -584,7 +584,7 @@ var roundTripTestCases = []FilterRoundTripTestCase{
func TestFilterMarshalJSON(t *testing.T) {
for _, tc := range marshalTestCases {
t.Run(tc.name, func(t *testing.T) {
result, err := tc.filter.MarshalJSON()
result, err := MarshalJSON(tc.filter)
assert.NoError(t, err)
var expectedMap, actualMap map[string]interface{}
@@ -602,7 +602,7 @@ func TestFilterUnmarshalJSON(t *testing.T) {
for _, tc := range unmarshalTestCases {
t.Run(tc.name, func(t *testing.T) {
var result Filter
err := result.UnmarshalJSON([]byte(tc.input))
err := UnmarshalJSON([]byte(tc.input), &result)
assert.NoError(t, err)
expectEqualFilters(t, result, tc.expected)
@@ -613,11 +613,11 @@ func TestFilterUnmarshalJSON(t *testing.T) {
func TestFilterRoundTrip(t *testing.T) {
for _, tc := range roundTripTestCases {
t.Run(tc.name, func(t *testing.T) {
jsonBytes, err := tc.filter.MarshalJSON()
jsonBytes, err := MarshalJSON(tc.filter)
assert.NoError(t, err)
var result Filter
err = result.UnmarshalJSON(jsonBytes)
err = UnmarshalJSON(jsonBytes, &result)
assert.NoError(t, err)
expectEqualFilters(t, result, tc.filter)

View File

@@ -391,7 +391,7 @@ func TestEventFilterMatching(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
actualIDs := []string{}
for _, event := range testEvents {
if tc.filter.Matches(&event) {
if Matches(tc.filter, event) {
actualIDs = append(actualIDs, event.ID[:8])
}
}
@@ -416,5 +416,5 @@ func TestEventFilterMatchingSkipMalformedTags(t *testing.T) {
},
}
assert.True(t, filter.Matches(&event))
assert.True(t, Matches(filter, event))
}