251 lines
7.4 KiB
Markdown
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 ./...
|
|
```
|