Rewrite README around ValidatedEvent as the primary consumer type

This commit is contained in:
Jay
2026-05-22 13:05:24 -04:00
parent d699feb236
commit a765a2262a
+99 -198
View File
@@ -1,4 +1,4 @@
# Go-Roots - Nostr Protocol Library for Golang # go-roots Nostr Protocol Library for Go
Source: https://git.wisehodl.dev/jay/go-roots Source: https://git.wisehodl.dev/jay/go-roots
@@ -6,22 +6,18 @@ Mirror: https://github.com/wisehodl/go-roots
## What this library does ## What this library does
`go-roots` is a consensus-layer Nostr protocol library for golang. `go-roots` is a consensus-layer Nostr protocol library for Go.
It only provides primitives that define protocol compliance: It provides primitives that define protocol compliance:
- Event Structure - Event structure and serialization
- Serialization - Cryptographic signing and validation
- Cryptographic Signatures - Subscription filters
- Subscription Filters
## What this library does not do ## What this library does not do
`go-roots` serves a foundation for other libraries and applications to `go-roots` serves as a foundation for libraries and applications that implement higher-level abstractions of the Nostr protocol, including message transport, semantic event definitions, event storage, and user interfaces.
implement higher level abstractions of the Nostr protocol on top of it,
including message transport, semantic event definitions, event storage
mechanisms, and user interfaces.
`go-roots` prioritizes correctness and clarity over optimization and efficiency. For high performance applications, it is recommended to implement optimizations in a separate library or in the application which requires them. `go-roots` prioritizes correctness and clarity over optimization and efficiency. High-performance applications should implement optimizations in a separate library or in the application itself.
## Installation ## Installation
@@ -31,7 +27,7 @@ mechanisms, and user interfaces.
go get git.wisehodl.dev/jay/go-roots go get git.wisehodl.dev/jay/go-roots
``` ```
If the primary repository is unavailable, use the `replace` directive in your go.mod file to get the package from the github mirror: If the primary repository is unavailable, use the `replace` directive in your go.mod file to get the package from the GitHub mirror:
``` ```
replace git.wisehodl.dev/jay/go-roots => github.com/wisehodl/go-roots latest replace git.wisehodl.dev/jay/go-roots => github.com/wisehodl/go-roots latest
@@ -39,7 +35,7 @@ replace git.wisehodl.dev/jay/go-roots => github.com/wisehodl/go-roots latest
2. Import the packages: 2. Import the packages:
```golang ```go
import ( import (
"git.wisehodl.dev/jay/go-roots/errors" "git.wisehodl.dev/jay/go-roots/errors"
"git.wisehodl.dev/jay/go-roots/events" "git.wisehodl.dev/jay/go-roots/events"
@@ -48,9 +44,9 @@ import (
) )
``` ```
3. Access functions with appropriate namespaces. ## General Use
## Usage Examples `ValidatedEvent` is the primary type consumers work with. A plain `Event` is a mutable scratch pad used during construction or deserialization. Once an event passes cryptographic validation, it is promoted to `ValidatedEvent` through a single gate: `NewValidatedEvent`. After that point, the event is immutable and its fields are accessed through methods.
### Key Management ### Key Management
@@ -68,7 +64,7 @@ if err != nil {
} }
``` ```
#### Derive public key from existing private key #### Derive public key from an existing private key
```go ```go
privateKey := "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167" privateKey := "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167"
@@ -76,132 +72,99 @@ publicKey, err := keys.GetPublicKey(privateKey)
// publicKey: "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef" // publicKey: "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef"
``` ```
--- ### Flow 1: Creating an event
### Event Creation and Signing Build the event with `NewEvent`, compute the ID with `GetID`, sign it with `SignEvent`, then promote to `ValidatedEvent` with `NewValidatedEvent`.
#### Create and sign a complete event
```go ```go
// 1. Build the event structure // 1. Build the event
event := events.Event{ event := events.NewEvent(
PubKey: publicKey, events.WithPubKey(publicKey),
CreatedAt: int(time.Now().Unix()), events.WithCreatedAt(time.Now().Unix()),
Kind: 1, events.WithKind(1),
Tags: []events.Tag{ events.WithTag(events.Tag{"e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}),
{"e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}, events.WithTag(events.Tag{"p", "91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"}),
{"p", "91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"}, events.WithContent("Hello, Nostr!"),
}, )
Content: "Hello, Nostr!",
}
// 2. Compute the event ID // 2. Compute and assign the ID
id := events.GetID(event) event.ID = events.GetID(event)
event.ID = id
// 3. Sign the event // 3. Sign the event
sig, err := events.SignEvent(id, privateKey) sig, err := events.SignEvent(event.ID, privateKey)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
event.Sig = sig event.Sig = sig
```
#### Serialize an event for ID computation // 4. Promote to ValidatedEvent
validated, err := events.NewValidatedEvent(event)
```go
// Returns canonical JSON: [0, pubkey, created_at, kind, tags, content]
serialized := events.Serialize(event)
```
#### Compute event ID manually
```go
id := events.GetID(event)
// Returns lowercase hex SHA-256 hash of serialized form
```
---
### Event Validation
#### Validate complete event
```go
// Checks structure, ID computation, and signature
if err := events.Validate(event); err != nil {
log.Printf("Invalid event: %v", err)
}
```
#### Validate individual aspects
```go
// Check field formats and lengths
if err := events.ValidateStructure(event); err != nil {
log.Printf("Malformed structure: %v", err)
}
// Verify ID matches computed hash
if err := events.ValidateID(event); err != nil {
log.Printf("ID mismatch: %v", err)
}
// Verify cryptographic signature
if err := events.ValidateSignature(event); err != nil {
log.Printf("Invalid signature: %v", err)
}
```
---
### Event JSON
#### Marshal event to JSON
```go
jsonBytes, err := json.Marshal(event)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err) // construction bug — treat as a defect, not a runtime condition
} }
// Standard encoding/json works with Event struct tags
``` ```
#### Unmarshal event from JSON For applications that delegate signing to a remote signer (NIP-46, hardware device, custody service): build the unsigned `Event`, send it to the signer, receive the signed `Event` back, then call `NewValidatedEvent` on what you received.
### Flow 2: Receiving an event from an external source
Unmarshal into a plain `Event`, then immediately promote to `ValidatedEvent` with `NewValidatedEvent`.
```go ```go
var event events.Event var event events.Event
err := json.Unmarshal(jsonBytes, &event) if err := json.Unmarshal(data, &event); err != nil {
if err != nil { log.Printf("Malformed JSON: %v", err)
log.Fatal(err) return
} }
// Validate after unmarshaling validated, err := events.NewValidatedEvent(event)
if err := events.Validate(event); err != nil { if err != nil {
log.Printf("Received invalid event: %v", err) log.Printf("Invalid event: %v", err) // reject from peer; investigate if from database
return
} }
``` ```
--- ### Flow 3: Serializing a validated event to JSON
### Filter Creation Call `json.Marshal` on a `ValidatedEvent`. The output is guaranteed to reflect a cryptographically verified event.
#### Basic filter with standard fields ```go
jsonBytes, err := json.Marshal(validated)
if err != nil {
log.Fatal(err)
}
```
Calling `json.Marshal` on a plain `Event` is discouraged: the resulting JSON carries no integrity guarantee.
### The `NewValidatedEvent` gate
`NewValidatedEvent` is the single promotion point. It verifies:
- Valid field structure (hex lengths, tag shape, field formats)
- ID matches the SHA-256 hash of the canonical serialization
- Signature is a valid Schnorr signature over the ID for the given public key
It does not verify semantic validity. A `ValidatedEvent` is valid according to the base Nostr protocol. Whether it is valid for your application — correct kind, trusted author, within a relevant time window — is your responsibility.
### Filters
#### Build a filter
```go ```go
since := int(time.Now().Add(-24 * time.Hour).Unix()) since := int(time.Now().Add(-24 * time.Hour).Unix())
limit := 50 limit := 50
filter := filters.Filter{ filter := filters.Filter{
IDs: []string{"abc123", "def456"}, // Prefix match IDs: []string{"abc123", "def456"}, // prefix match
Authors: []string{"cfa87f35"}, // Prefix match Authors: []string{"cfa87f35"}, // prefix match
Kinds: []int{1, 6, 7}, Kinds: []int{1, 6, 7},
Since: &since, Since: &since,
Limit: &limit, Limit: &limit,
} }
``` ```
#### Filter with tag conditions #### Tag filters
```go ```go
filter := filters.Filter{ filter := filters.Filter{
@@ -213,27 +176,9 @@ filter := filters.Filter{
} }
``` ```
#### Filter with extensions (custom fields) #### Match events against a filter
```go `Matches` accepts a `ValidatedEvent`. Unvalidated events cannot be passed to it.
// Extensions allow arbitrary JSON fields beyond the standard filter spec.
// For example, this is how to implement non-standard filters like 'search'.
filter := filters.Filter{
Kinds: []int{1},
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"bitcoin"`),
},
}
// Extensions are preserved during marshal/unmarshal but ignored by Matches().
// Storage/transport layers can inspect Extensions to implement custom behavior.
```
---
### Filter Matching
#### Match single event
```go ```go
filter := filters.Filter{ filter := filters.Filter{
@@ -241,88 +186,33 @@ filter := filters.Filter{
Kinds: []int{1}, Kinds: []int{1},
} }
if filters.Matches(filter, event) { var matched []events.ValidatedEvent
// Event satisfies all filter conditions for _, event := range incoming {
}
```
#### Filter event collection
```go
since := int(time.Now().Add(-1 * time.Hour).Unix())
filter := filters.Filter{
Kinds: []int{1},
Since: &since,
Tags: filters.TagFilters{
"p": {"abc123", "def456"}, // OR within tag values
},
}
var matches []events.Event
for _, event := range events {
if filters.Matches(filter, event) { if filters.Matches(filter, event) {
matches = append(matches, event) matched = append(matched, event)
} }
} }
``` ```
--- `Matches` covers the standard NIP-01 filter fields. For non-trivial matching logic, implement your own.
### Filter JSON #### Filter JSON
#### Marshal filter to JSON Tag filter keys are prefixed with `#` in JSON (`#e`, `#p`). Unrecognized fields are captured in `Extensions` and are retained as-found. `Matches` ignores `Extensions`; higher layers can inspect them for custom behavior.
```go ```go
filter := filters.Filter{ // Marshal
IDs: []string{"abc123"},
Kinds: []int{1},
Tags: filters.TagFilters{
"e": {"event-id"},
},
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"nostr"`),
},
}
jsonBytes, err := filters.MarshalJSON(filter) jsonBytes, err := filters.MarshalJSON(filter)
// Result: {"ids":["abc123"],"kinds":[1],"#e":["event-id"],"search":"nostr"} // {"authors":["cfa87f35"],"kinds":[1],"#e":["..."],"since":...}
```
#### Unmarshal filter from JSON // Unmarshal
var f filters.Filter
```go if err := filters.UnmarshalJSON(data, &f); err != nil {
jsonData := `{
"authors": ["cfa87f35"],
"kinds": [1],
"#e": ["abc123"],
"since": 1234567890,
"search": "bitcoin"
}`
var filter filters.Filter
err := filters.UnmarshalJSON([]byte(jsonData), &filter)
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Standard fields populated: Authors, Kinds, Since
// Tag filters populated: Tags["e"] = ["abc123"]
// Unknown fields populated: Extensions["search"] = "bitcoin"
``` ```
#### Extensions field behavior Extensions example:
The `Extensions` field captures any JSON properties not recognized as standard filter fields or tag filters. This design allows the core library to remain frozen while storage and transport layers implement custom filtering behavior.
**Standard fields**: `ids`, `authors`, `kinds`, `since`, `until`, `limit`
**Tag filters**: Any key starting with `#` (e.g., `#e`, `#p`, `#emoji`)
**Extensions**: Everything else
During marshaling, Extensions merge into the output JSON. During unmarshaling, unrecognized fields populate Extensions. The `Matches()` method ignores Extensions, and the library expects higher protocol layers to implement their usage.
Example implementing search filter:
```go ```go
filter := filters.Filter{ filter := filters.Filter{
@@ -334,15 +224,26 @@ filter := filters.Filter{
// In a storage layer (not this library): // In a storage layer (not this library):
if searchRaw, ok := filter.Extensions["search"]; ok { if searchRaw, ok := filter.Extensions["search"]; ok {
var searchTerm string var term string
json.Unmarshal(searchRaw, &searchTerm) json.Unmarshal(searchRaw, &term)
// Apply full-text search using searchTerm // apply full-text search
} }
``` ```
## Testing ---
This library contains a comprehensive suite of unit tests. Run them with: ## Specialized / Low-Level Tools
These are building blocks for custom pipelines. General use does not require them.
- **`Validate(e Event) error`** — full validation in one call; used internally by `NewValidatedEvent`
- **`ValidateStructure(e Event) error`** — field format checks only
- **`ValidateSignature(e Event) error`** — Schnorr signature verification only
- **`Serialize(e Event) []byte`** — canonical `[0, pubkey, created_at, kind, tags, content]` JSON array; used for ID computation
- **`GetID(e Event) string`** — SHA-256 of `Serialize`, returned as lowercase hex
- **`IsValidKey(s string) bool`**, **`IsValidID`**, **`IsValidSig`** — format validators for individual field values; useful when handling these value types outside of event validation
## Testing
```bash ```bash
go test ./... go test ./...