Files
go-roots/README.md
T

251 lines
7.4 KiB
Markdown

# go-roots — Nostr Protocol Library for Go
Source: https://git.wisehodl.dev/jay/go-roots
Mirror: https://github.com/wisehodl/go-roots
## What this library does
`go-roots` is a consensus-layer Nostr protocol library for Go.
It provides primitives that define protocol compliance:
- Event structure and serialization
- Cryptographic signing and validation
- Subscription filters
## What this library does not do
`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.
`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
1. Add `go-roots` to your project:
```bash
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:
```
replace git.wisehodl.dev/jay/go-roots => github.com/wisehodl/go-roots latest
```
2. Import the packages:
```go
import (
"git.wisehodl.dev/jay/go-roots/errors"
"git.wisehodl.dev/jay/go-roots/events"
"git.wisehodl.dev/jay/go-roots/filters"
"git.wisehodl.dev/jay/go-roots/keys"
)
```
## General Use
`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
#### Generate a new keypair
```go
privateKey, err := keys.GeneratePrivateKey()
if err != nil {
log.Fatal(err)
}
publicKey, err := keys.GetPublicKey(privateKey)
if err != nil {
log.Fatal(err)
}
```
#### Derive public key from an existing private key
```go
privateKey := "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167"
publicKey, err := keys.GetPublicKey(privateKey)
// publicKey: "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef"
```
### Flow 1: Creating an event
Build the event with `NewEvent`, compute the ID with `GetID`, sign it with `SignEvent`, then promote to `ValidatedEvent` with `NewValidatedEvent`.
```go
// 1. Build the event
event := events.NewEvent(
events.WithPubKey(publicKey),
events.WithCreatedAt(time.Now().Unix()),
events.WithKind(1),
events.WithTag(events.Tag{"e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}),
events.WithTag(events.Tag{"p", "91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"}),
events.WithContent("Hello, Nostr!"),
)
// 2. Compute and assign the ID
event.ID = events.GetID(event)
// 3. Sign the event
sig, err := events.SignEvent(event.ID, privateKey)
if err != nil {
log.Fatal(err)
}
event.Sig = sig
// 4. Promote to ValidatedEvent
validated, err := events.NewValidatedEvent(event)
if err != nil {
log.Fatal(err) // construction bug — treat as a defect, not a runtime condition
}
```
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
var event events.Event
if err := json.Unmarshal(data, &event); err != nil {
log.Printf("Malformed JSON: %v", err)
return
}
validated, err := events.NewValidatedEvent(event)
if err != nil {
log.Printf("Invalid event: %v", err) // reject from peer; investigate if from database
return
}
```
### Flow 3: Serializing a validated event to JSON
Call `json.Marshal` on a `ValidatedEvent`. The output is guaranteed to reflect a cryptographically verified event.
```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
since := int(time.Now().Add(-24 * time.Hour).Unix())
limit := 50
filter := filters.Filter{
IDs: []string{"abc123", "def456"}, // prefix match
Authors: []string{"cfa87f35"}, // prefix match
Kinds: []int{1, 6, 7},
Since: &since,
Limit: &limit,
}
```
#### Tag filters
```go
filter := filters.Filter{
Kinds: []int{1},
Tags: filters.TagFilters{
"e": {"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"},
"p": {"91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"},
},
}
```
#### Match events against a filter
`Matches` accepts a `ValidatedEvent`. Unvalidated events cannot be passed to it.
```go
filter := filters.Filter{
Authors: []string{"cfa87f35"},
Kinds: []int{1},
}
var matched []events.ValidatedEvent
for _, event := range incoming {
if filters.Matches(filter, event) {
matched = append(matched, event)
}
}
```
`Matches` covers the standard NIP-01 filter fields. For non-trivial matching logic, implement your own.
#### Filter 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
// Marshal
jsonBytes, err := filters.MarshalJSON(filter)
// {"authors":["cfa87f35"],"kinds":[1],"#e":["..."],"since":...}
// Unmarshal
var f filters.Filter
if err := filters.UnmarshalJSON(data, &f); err != nil {
log.Fatal(err)
}
```
Extensions example:
```go
filter := filters.Filter{
Kinds: []int{1},
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"bitcoin"`),
},
}
// In a storage layer (not this library):
if searchRaw, ok := filter.Extensions["search"]; ok {
var term string
json.Unmarshal(searchRaw, &term)
// apply full-text search
}
```
---
## 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
go test ./...
```