4 Commits
v0.2.0 ... main

Author SHA1 Message Date
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
Jay
4df91938ef Refactored methods into pure functions. 2025-10-31 19:48:56 -04:00
11 changed files with 65 additions and 44 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 ## 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: It only provides primitives that define protocol compliance:
- Event Structure - Event Structure
@@ -96,7 +96,7 @@ event := events.Event{
} }
// 2. Compute the event ID // 2. Compute the event ID
id, err := event.GetID() id, err := events.GetID(event)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -114,7 +114,7 @@ event.Sig = sig
```go ```go
// Returns canonical JSON: [0, pubkey, created_at, kind, tags, content] // Returns canonical JSON: [0, pubkey, created_at, kind, tags, content]
serialized, err := event.Serialize() serialized, err := events.Serialize(event)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -123,7 +123,7 @@ if err != nil {
#### Compute event ID manually #### Compute event ID manually
```go ```go
id, err := event.GetID() id, err := events.GetID(event)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -138,7 +138,7 @@ if err != nil {
```go ```go
// Checks structure, ID computation, and signature // 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) log.Printf("Invalid event: %v", err)
} }
``` ```
@@ -147,17 +147,17 @@ if err := event.Validate(); err != nil {
```go ```go
// Check field formats and lengths // Check field formats and lengths
if err := event.ValidateStructure(); err != nil { if err := events.ValidateStructure(event); err != nil {
log.Printf("Malformed structure: %v", err) log.Printf("Malformed structure: %v", err)
} }
// Verify ID matches computed hash // Verify ID matches computed hash
if err := event.ValidateID(); err != nil { if err := events.ValidateID(event); err != nil {
log.Printf("ID mismatch: %v", err) log.Printf("ID mismatch: %v", err)
} }
// Verify cryptographic signature // Verify cryptographic signature
if err := event.ValidateSignature(); err != nil { if err := events.ValidateSignature(event); err != nil {
log.Printf("Invalid signature: %v", err) log.Printf("Invalid signature: %v", err)
} }
``` ```
@@ -186,7 +186,7 @@ if err != nil {
} }
// Validate after unmarshaling // Validate after unmarshaling
if err := event.Validate(); err != nil { if err := events.Validate(event); err != nil {
log.Printf("Received invalid event: %v", err) log.Printf("Received invalid event: %v", err)
} }
``` ```
@@ -250,7 +250,7 @@ filter := filters.Filter{
Kinds: []int{1}, Kinds: []int{1},
} }
if filter.Matches(&event) { if filters.Matches(filter, event) {
// Event satisfies all filter conditions // Event satisfies all filter conditions
} }
``` ```
@@ -269,7 +269,7 @@ filter := filters.Filter{
var matches []events.Event var matches []events.Event
for _, event := range events { for _, event := range events {
if filter.Matches(&event) { if filters.Matches(filter, event) {
matches = append(matches, 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"} // Result: {"ids":["abc123"],"kinds":[1],"#e":["event-id"],"search":"nostr"}
``` ```
@@ -309,7 +309,7 @@ jsonData := `{
}` }`
var filter filters.Filter var filter filters.Filter
err := filter.UnmarshalJSON([]byte(jsonData)) err := filters.UnmarshalJSON([]byte(jsonData), &filter)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

2
c2p
View File

@@ -1 +1 @@
code2prompt -e "go.sum" -e "README.md" -e "c2p" . code2prompt -e "go.sum" -e "c2p" .

View File

@@ -9,7 +9,7 @@ import (
func TestUnmarshalEventJSON(t *testing.T) { func TestUnmarshalEventJSON(t *testing.T) {
event := Event{} event := Event{}
json.Unmarshal(testEventJSONBytes, &event) json.Unmarshal(testEventJSONBytes, &event)
if err := event.Validate(); err != nil { if err := Validate(event); err != nil {
t.Error("unmarshalled event is invalid") t.Error("unmarshalled event is invalid")
} }
expectEqualEvents(t, event, testEvent) 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"}` 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") t.Error("test event is invalid")
} }
eventJSON, err := json.Marshal(event) eventJSON, err := json.Marshal(event)
@@ -47,7 +47,7 @@ func TestEventJSONRoundTrip(t *testing.T) {
unmarshalledEvent := Event{} unmarshalledEvent := Event{}
json.Unmarshal(eventJSON, &unmarshalledEvent) json.Unmarshal(eventJSON, &unmarshalledEvent)
if err := unmarshalledEvent.Validate(); err != nil { if err := Validate(unmarshalledEvent); err != nil {
t.Error("unmarshalled event is invalid") t.Error("unmarshalled event is invalid")
} }
expectEqualEvents(t, unmarshalledEvent, event) expectEqualEvents(t, unmarshalledEvent, event)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -391,7 +391,7 @@ func TestEventFilterMatching(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actualIDs := []string{} actualIDs := []string{}
for _, event := range testEvents { for _, event := range testEvents {
if tc.filter.Matches(&event) { if Matches(tc.filter, event) {
actualIDs = append(actualIDs, event.ID[:8]) 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))
} }