7.4 KiB
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
- Add
go-rootsto your project:
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
- Import the packages:
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
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
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.
// 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.
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.
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
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
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.
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.
// 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:
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 byNewValidatedEventValidateStructure(e Event) error— field format checks onlyValidateSignature(e Event) error— Schnorr signature verification onlySerialize(e Event) []byte— canonical[0, pubkey, created_at, kind, tags, content]JSON array; used for ID computationGetID(e Event) string— SHA-256 ofSerialize, returned as lowercase hexIsValidKey(s string) bool,IsValidID,IsValidSig— format validators for individual field values; useful when handling these value types outside of event validation
Testing
go test ./...