Nostr Library
A comprehensive, high-performance Go implementation of the Nostr protocol providing encoding/decoding, cryptography, WebSocket client, and relay utilities. This library offers a complete toolkit for building Nostr clients and relays with optimized performance through SIMD acceleration and zero-copy operations.
Features
- Pure Go with Purego - No CGO dependencies, uses
ebitengine/puregoto dynamically loadlibsecp256k1.so - SIMD Optimization - Accelerated SHA256 (
minio/sha256-simd) and hex encoding (templexxx/xhex) - Zero-Copy Operations - Efficient buffer pooling and reuse
- Complete NIP Support - Events, filters, tags, envelopes, and cryptographic operations
- WebSocket Client - Full-featured client for connecting to Nostr relays
- Relay Information - NIP-11 relay information document handling
- HTTP Authentication - NIP-98 HTTP auth for REST APIs
- Multiple Encodings - JSON, binary, and canonical encodings for events
- Comprehensive Crypto - Schnorr signatures, ECDH, NIP-04/NIP-44 encryption, MuSig2
Installation
go get git.mleku.dev/mleku/nostr
Quick Start
Connect to a Relay and Subscribe
package main
import (
"context"
"fmt"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
// Connect to relay
relay, err := ws.RelayConnect(ctx, "wss://relay.damus.io")
if err != nil {
panic(err)
}
defer relay.Close()
// Create filter
f := filter.New()
f.Kinds = kind.NewS(kind.TextNote)
limit := 10
f.Limit = &limit
// Subscribe to events
sub, err := relay.Subscribe(ctx, filter.NewS(f))
if err != nil {
panic(err)
}
// Process events
for event := range sub.Events {
fmt.Printf("Event: %s\n", string(event.Content))
}
}
Table of Contents
- WebSocket Client - Connect to Nostr relays
- Event Package - Event encoding/decoding and manipulation
- Filter Package - Query filters for event subscriptions
- Tag Package - Tag encoding and manipulation
- Kind Package - Event kind constants and utilities
- Envelope Packages - WebSocket message envelopes
- Relay Information - NIP-11 relay info documents
- HTTP Authentication - NIP-98 HTTP auth
- Cryptography - Signatures, encryption, and key management
- Protocol Helpers - NIP-42 auth and utilities
- Utilities - Helpers and tools
WebSocket Client
The ws package provides a complete WebSocket client implementation for connecting to Nostr relays.
Features
- Connect to relays with context-based lifecycle management
- Subscribe to events with filters
- Publish events with OK callbacks
- NIP-42 authentication support
- Automatic EOSE (End Of Stored Events) detection
- Duplicate event checking
- Replaceable event handling
- Custom message handlers
- Notice handling
Connecting to a Relay
package main
import (
"context"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
// Connect to relay
relay, err := ws.RelayConnect(ctx, "wss://relay.damus.io")
if err != nil {
panic(err)
}
defer relay.Close()
// Relay is now ready to use
}
Subscribing to Events
package main
import (
"context"
"fmt"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
relay, _ := ws.RelayConnect(ctx, "wss://relay.damus.io")
defer relay.Close()
// Create filters
f := filter.New()
f.Kinds = kind.NewS(kind.TextNote, kind.Reaction)
limit := 100
f.Limit = &limit
// Subscribe with options
sub, err := relay.Subscribe(ctx, filter.NewS(f),
ws.WithLabel("my-subscription"),
)
if err != nil {
panic(err)
}
// Process events as they arrive
for event := range sub.Events {
fmt.Printf("Event %x: %s\n", event.ID, string(event.Content))
}
// Check if EOSE was received
if sub.EndOfStoredEvents.Load() {
fmt.Println("Received all stored events")
}
}
Publishing Events
package main
import (
"context"
"fmt"
"time"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
relay, _ := ws.RelayConnect(ctx, "wss://relay.damus.io")
defer relay.Close()
// Create signer
signer, _ := p8k.New()
signer.Generate()
// Create event
ev := event.New()
ev.Kind = kind.TextNote.K
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("Hello Nostr!")
ev.Sign(signer)
// Publish event
err := relay.Publish(ctx, ev)
if err != nil {
panic(err)
}
fmt.Printf("Published event: %x\n", ev.ID)
}
NIP-42 Authentication
package main
import (
"context"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
relay, _ := ws.RelayConnect(ctx, "wss://relay.example.com")
defer relay.Close()
// Create signer
signer, _ := p8k.New()
signer.Generate()
// Authenticate with relay
err := relay.Auth(ctx, signer)
if err != nil {
panic(err)
}
}
Subscription Options
// WithLabel sets the subscription label
sub, _ := relay.Subscribe(ctx, filters, ws.WithLabel("my-sub"))
// Subscription automatically closes when context is cancelled
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
sub, _ := relay.Subscribe(ctx, filters)
Event Package
The event package provides the core Nostr event type with JSON, binary, and canonical encodings.
Types
type E struct {
ID []byte // SHA256 hash of canonical encoding
Pubkey []byte // Public key (32 bytes)
CreatedAt int64 // UNIX timestamp
Kind uint16 // Event kind
Tags *tag.S // Tag list
Content []byte // Arbitrary content
Sig []byte // Schnorr signature (64 bytes)
}
type S []*E // Slice of events (sortable by CreatedAt)
type C chan *E // Channel for event streaming
Constructor
func New() *E
Methods
Lifecycle
func (ev *E) Free() // Nil all fields for GC
func (ev *E) Clone() *E // Deep copy with independent memory
func (ev *E) EstimateSize() int // Estimate serialized size
Encoding/Decoding
func (ev *E) Marshal(dst []byte) []byte // Marshal to JSON
func (ev *E) MarshalJSON() ([]byte, error) // Standard JSON marshaler
func (ev *E) Serialize() []byte // Marshal to new buffer
func (ev *E) Unmarshal(b []byte) ([]byte, error) // Unmarshal from JSON
func (ev *E) UnmarshalJSON(b []byte) error // Standard JSON unmarshaler
Binary Encoding
func (ev *E) MarshalBinary(w io.Writer) // Write binary format
func (ev *E) MarshalBinaryToBytes(dst []byte) []byte // Binary to bytes
func (ev *E) UnmarshalBinary(r io.Reader) error // Read binary format
Canonical Encoding
func (ev *E) ToCanonical(dst []byte) []byte // Canonical encoding for ID
func (ev *E) GetIDBytes() []byte // Compute event ID
func Hash(in []byte) []byte // SHA256 hash
Signatures
func (ev *E) Sign(keys signer.I) error // Sign event with key pair
func (ev *E) Verify() (bool, error) // Verify signature
Usage Example
package main
import (
"fmt"
"time"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/tag"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
// Create a signer
signer, _ := p8k.New()
signer.Generate()
// Create an event
ev := event.New()
ev.Kind = kind.TextNote.K
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("Hello Nostr!")
// Add tags
ev.Tags = tag.NewS(
tag.NewFromBytesSlice([]byte("t"), []byte("nostr")),
tag.NewFromBytesSlice([]byte("p"), signer.Pub()),
)
// Sign the event
if err := ev.Sign(signer); err != nil {
panic(err)
}
// Verify signature
valid, err := ev.Verify()
if err != nil || !valid {
panic("invalid signature")
}
// Marshal to JSON
jsonBytes := ev.Serialize()
fmt.Printf("Event JSON: %s\n", jsonBytes)
// Unmarshal from JSON
ev2 := event.New()
_, err = ev2.Unmarshal(jsonBytes)
if err != nil {
panic(err)
}
// Clone for async processing
clone := ev.Clone()
go processEvent(clone)
// Free original
ev.Free()
}
func processEvent(ev *event.E) {
defer ev.Free()
// Process event...
}
Filter Package
The filter package implements Nostr subscription filters for querying events.
Type
type F struct {
IDs *schnorr.Bytes // Event IDs (hex-encoded in JSON)
Authors *schnorr.Bytes // Author pubkeys (hex-encoded in JSON)
Kinds *kind.S // Event kinds
Tags *tag.S // Tag filters (#e, #p, etc.)
Since *int64 // UNIX timestamp
Until *int64 // UNIX timestamp
Limit *int // Max results
}
Constructor
func New() *F
Methods
func (f *F) Sort() // Sort fields for deterministic output
func (f *F) Matches(ev *event.E) bool // Check if event matches filter
func (f *F) MatchesIgnoringTimestampConstraints(ev *event.E) bool
func (f *F) EstimateSize() int // Estimate serialized size
func (f *F) Marshal(dst []byte) []byte // Marshal to JSON
func (f *F) Serialize() []byte // Marshal to new buffer
func (f *F) Unmarshal(b []byte) ([]byte, error) // Unmarshal from JSON
Usage Example
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/crypto/ec/schnorr"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/tag"
)
func main() {
// Create a filter
f := filter.New()
// Filter by kinds
f.Kinds = kind.NewS(kind.TextNote, kind.EncryptedDirectMessage)
// Filter by authors
pubkey, _ := schnorr.ParseBytes("...")
f.Authors = schnorr.NewBytes(pubkey)
// Time constraints
since := int64(1234567890)
until := int64(9999999999)
limit := 100
f.Since = &since
f.Until = &until
f.Limit = &limit
// Tag filters (e.g., #p tag)
f.Tags = tag.NewS(
tag.NewFromBytesSlice([]byte("#p"), pubkey),
)
// Sort for deterministic output
f.Sort()
// Marshal to JSON
jsonBytes := f.Serialize()
fmt.Printf("Filter: %s\n", jsonBytes)
// Check if event matches
ev := event.New()
ev.Kind = kind.TextNote.K
if f.Matches(ev) {
fmt.Println("Event matches filter")
}
}
Tag Package
The tag package provides tag encoding and manipulation.
Types
type T struct {
T [][]byte // Tag elements (first is key, rest are values)
}
type S []*T // Slice of tags
Constructors
func New() *T
func NewWithCap(c int) *T
func NewFromBytesSlice(t ...[]byte) *T
func NewFromAny(t ...any) *T
func NewS(t ...*T) *S
func NewSWithCap(c int) *S
Tag Methods
func (t *T) Free() // Nil all fields
func (t *T) Len() int // Number of elements
func (t *T) Less(i, j int) bool // Compare elements
func (t *T) Swap(i, j int) // Swap elements
func (t *T) Contains(s []byte) bool // Check if contains element
func (t *T) Marshal(dst []byte) []byte // Marshal to JSON
func (t *T) MarshalJSON() ([]byte, error) // Standard marshaler
func (t *T) Unmarshal(b []byte) ([]byte, error) // Unmarshal from JSON
func (t *T) UnmarshalJSON(b []byte) error // Standard unmarshaler
func (t *T) Key() []byte // Get first element (key)
func (t *T) Value() []byte // Get second element (value)
func (t *T) Relay() []byte // Get third element (relay)
func (t *T) ValueHex() []byte // Get value as hex
func (t *T) ValueBinary() []byte // Get value as binary
func (t *T) ToSliceOfStrings() []string // Convert to string slice
func (t *T) Equals(other *T) bool // Compare tags
Tag Slice Methods
func (s *S) Len() int
func (s *S) Less(i, j int) bool
func (s *S) Swap(i, j int)
func (s *S) Append(t ...*T)
func (s *S) Marshal(dst []byte) []byte
func (s *S) Unmarshal(b []byte) ([]byte, error)
func (s *S) GetFirst(tagName []byte) *T // Get first tag with key
func (s *S) GetAll(tagName []byte) []*T // Get all tags with key
func (s *S) FilterOut(tagName []byte) *S // Remove tags with key
Usage Example
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/encoders/tag"
)
func main() {
// Create individual tags
eTag := tag.NewFromBytesSlice(
[]byte("e"),
hex.Dec("...event-id..."),
[]byte("wss://relay.example.com"),
[]byte("reply"),
)
pTag := tag.NewFromBytesSlice(
[]byte("p"),
hex.Dec("...pubkey..."),
)
tTag := tag.NewFromAny("t", "nostr", "protocol")
// Create tag collection
tags := tag.NewS(eTag, pTag, tTag)
// Access tag elements
fmt.Printf("Key: %s\n", eTag.Key()) // "e"
fmt.Printf("Value: %s\n", eTag.ValueHex()) // event-id as hex (handles binary storage)
fmt.Printf("Relay: %s\n", eTag.Relay()) // relay URL
// Find tags - use ValueHex() for e/p tags (may be binary-encoded internally)
pTags := tags.GetAll([]byte("p"))
for _, pt := range pTags {
fmt.Printf("P tag: %s\n", pt.ValueHex()) // Always returns hex regardless of storage
}
// Filter tags
withoutE := tags.FilterOut([]byte("e"))
// Marshal to JSON
jsonBytes := tags.Marshal(nil)
fmt.Printf("Tags: %s\n", jsonBytes)
}
Kind Package
The kind package provides event kind constants and utilities.
Type
type K struct {
K uint16 // Kind number
}
type S struct {
K []*K // Slice of kinds (sortable)
}
Constructors
func New(k uint16) *K
func NewS(k ...*K) *S
func NewWithCap(c int) *S
func FromIntSlice(is []int) *S
Constants
var (
Metadata = New(0) // NIP-01
TextNote = New(1) // NIP-01
RecommendRelay = New(2) // NIP-01
Contacts = New(3) // NIP-02
EncryptedDirectMessage = New(4) // NIP-04
EventDeletion = New(5) // NIP-09
Repost = New(6) // NIP-18
Reaction = New(7) // NIP-25
BadgeAward = New(8) // NIP-58
ChannelCreation = New(40) // NIP-28
ChannelMetadata = New(41) // NIP-28
ChannelMessage = New(42) // NIP-28
ChannelHideMessage = New(43) // NIP-28
ChannelMuteUser = New(44) // NIP-28
FileMetadata = New(1063) // NIP-94
LiveChatMessage = New(1311) // NIP-53
// Replaceable events (10000-19999)
ProfileBadges = New(30008) // NIP-58
BadgeDefinition = New(30009) // NIP-58
// Ephemeral events (20000-29999)
Auth = New(22242) // NIP-42
// ... and 200+ more kinds
)
Methods
func (k *S) Len() int
func (k *S) Less(i, j int) bool
func (k *S) Swap(i, j int)
func (k *S) ToUint16() []uint16
func (k *S) Clone() *S
func (k *S) Contains(s uint16) bool
func (k *S) Equals(t1 *S) bool
func (k *S) Marshal(dst []byte) []byte
func (k *S) Unmarshal(b []byte) ([]byte, error)
func (k *S) IsPrivileged() bool // Check if contains privileged kinds
func (k *K) IsRegular() bool // 1000 <= k < 10000
func (k *K) IsReplaceable() bool // 10000 <= k < 20000 or k in [0,3]
func (k *K) IsEphemeral() bool // 20000 <= k < 30000
func (k *K) IsAddressable() bool // 30000 <= k < 40000
Usage Example
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/encoders/kind"
)
func main() {
// Use predefined constants
textNote := kind.TextNote
fmt.Printf("Text note kind: %d\n", textNote.K)
// Create custom kind
customKind := kind.New(30023)
// Check kind properties
if customKind.IsAddressable() {
fmt.Println("This is an addressable event")
}
// Create kind filter
kinds := kind.NewS(
kind.TextNote,
kind.Reaction,
kind.Repost,
)
// Check if contains kind
if kinds.Contains(1) {
fmt.Println("Contains text notes")
}
// Convert to uint16 slice
kindNumbers := kinds.ToUint16()
fmt.Printf("Kinds: %v\n", kindNumbers)
}
Envelope Packages
Envelope packages provide WebSocket message framing for the Nostr protocol.
Available Envelopes
eventenvelope- EVENT messages (client to relay)reqenvelope- REQ messages (subscription requests)closeenvelope- CLOSE messages (close subscription)eoseenvelope- EOSE messages (end of stored events)okenvelope- OK messages (command results)noticeenvelope- NOTICE messages (human-readable messages)authenvelope- AUTH messages (NIP-42 authentication)closedenvelope- CLOSED messages (subscription closed by relay)countenvelope- COUNT messages (event counting)
Common Pattern
All envelopes follow a similar pattern:
// Create envelope
env := eventenvelope.NewSubmission()
// Unmarshal from wire format
remainder, err := env.Unmarshal(wireBytes)
// Access data
event := env.E
// Marshal to wire format
wireBytes = env.Marshal(nil)
Event Envelope Example
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/encoders/envelopes/eventenvelope"
"git.mleku.dev/mleku/nostr/encoders/event"
)
func main() {
// Client submitting event
ev := event.New()
// ... populate event ...
submission := eventenvelope.NewSubmission()
submission.E = ev
wireBytes := submission.Marshal(nil)
// Send wireBytes over WebSocket
// Server receiving event
received := eventenvelope.NewSubmission()
_, err := received.Unmarshal(wireBytes)
if err != nil {
panic(err)
}
// Process received.E
fmt.Printf("Received event: %x\n", received.E.ID)
}
REQ Envelope Example
package main
import (
"git.mleku.dev/mleku/nostr/encoders/envelopes/reqenvelope"
"git.mleku.dev/mleku/nostr/encoders/filter"
)
func main() {
// Create subscription request
req := reqenvelope.New()
req.Label = []byte("my-subscription")
// Add filters
f1 := filter.New()
// ... configure filter ...
req.Filters = append(req.Filters, f1)
// Marshal
wireBytes := req.Marshal(nil)
// Send over WebSocket
}
OK Envelope Example
package main
import (
"git.mleku.dev/mleku/nostr/encoders/envelopes/okenvelope"
"git.mleku.dev/mleku/nostr/encoders/reason"
)
func main() {
// Create OK response
ok := okenvelope.New()
ok.ID = eventID
ok.OK = true
ok.Reason = reason.Duplicate.With(": event already exists")
// Marshal
wireBytes := ok.Marshal(nil)
// Send to client
}
Relay Information
The relayinfo package implements NIP-11 relay information document handling.
Features
- Fetch relay information from HTTP endpoint
- Comprehensive NIP database (80+ NIPs with descriptions)
- Relay limits and restrictions
- Payment/fee structures
- Save/load relay info to/from JSON files
Types
type T struct {
Name []byte
Description []byte
Pubkey []byte
Contact []byte
Nips []int
Software []byte
Version []byte
Limitation *Limitation
Payments_url []byte
Fees *Fees
Icon []byte
// ... additional fields
}
type Limitation struct {
MaxMessageLength *int
MaxSubscriptions *int
MaxFilters *int
MaxLimit *int
MaxSubidLength *int
MaxEventTags *int
MaxContentLength *int
MinPowDifficulty *int
AuthRequired *bool
PaymentRequired *bool
RestrictedWrites *bool
CreatedAtLowerLimit *int64
CreatedAtUpperLimit *int64
}
Methods
func Fetch(ctx context.Context, u []byte) (*T, error) // Fetch from relay
func Load(filePath []byte) (*T, error) // Load from file
func (t *T) Save(filePath []byte) error // Save to file
func (t *T) Marshal(dst []byte) []byte // Marshal to JSON
func (t *T) Unmarshal(b []byte) ([]byte, error) // Unmarshal from JSON
Usage Example
package main
import (
"context"
"fmt"
"git.mleku.dev/mleku/nostr/relayinfo"
)
func main() {
ctx := context.Background()
// Fetch relay information
info, err := relayinfo.Fetch(ctx, []byte("wss://relay.damus.io"))
if err != nil {
panic(err)
}
// Display relay info
fmt.Printf("Relay: %s\n", info.Name)
fmt.Printf("Description: %s\n", info.Description)
fmt.Printf("NIPs supported: %v\n", info.Nips)
fmt.Printf("Software: %s %s\n", info.Software, info.Version)
// Check limitations
if info.Limitation != nil {
if info.Limitation.MaxMessageLength != nil {
fmt.Printf("Max message length: %d\n", *info.Limitation.MaxMessageLength)
}
if info.Limitation.AuthRequired != nil && *info.Limitation.AuthRequired {
fmt.Println("Authentication required")
}
}
// Save to file
err = info.Save([]byte("relay-info.json"))
if err != nil {
panic(err)
}
// Load from file
loaded, err := relayinfo.Load([]byte("relay-info.json"))
if err != nil {
panic(err)
}
}
HTTP Authentication
The httpauth package implements NIP-98 HTTP authentication for REST APIs.
Features
- Create NIP-98 authentication events
- Add Authorization headers to HTTP requests
- Support for payload hashing
- Expiration support
- Custom JWT-style delegation (kind 13004)
Functions
func AddNIP98Header(req *http.Request, u *url.URL, method, payload string,
signer signer.I, expiration int64) error
func CreateAuthEvent(u *url.URL, method, payload string,
signer signer.I, expiration int64) (*event.E, error)
Usage Example
package main
import (
"net/http"
"net/url"
"time"
"git.mleku.dev/mleku/nostr/httpauth"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
// Create signer
signer, _ := p8k.New()
signer.Generate()
// Create HTTP request
u, _ := url.Parse("https://api.example.com/upload")
req, _ := http.NewRequest("POST", u.String(), nil)
// Add NIP-98 auth header (expires in 1 hour)
expiration := time.Now().Add(1 * time.Hour).Unix()
err := httpauth.AddNIP98Header(req, u, "POST", "", signer, expiration)
if err != nil {
panic(err)
}
// Make request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}
Cryptography
Signer Interface
The signer.I interface abstracts key management and cryptographic operations.
type I interface {
Generate() error // Generate new key pair
InitSec(sec []byte) error // Load secret key
InitPub(pub []byte) error // Load public key
Sec() []byte // Get secret key
Pub() []byte // Get public key (x-only)
Sign(msg []byte) ([]byte, error) // Sign message
Verify(msg, sig []byte) (bool, error) // Verify signature
Zero() // Wipe secret key
ECDH(pub []byte) ([]byte, error) // Derive shared secret
ECDHRaw(pub []byte) ([]byte, error) // Raw ECDH (for NIP-44)
}
P8K Implementation
The p8k package provides implementations using both libsecp256k1 (via purego) and pure Go fallback.
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
// Create new signer
signer, err := p8k.New()
if err != nil {
panic(err)
}
// Generate random key pair
if err := signer.Generate(); err != nil {
panic(err)
}
fmt.Printf("Public key: %x\n", signer.Pub())
fmt.Printf("Secret key: %x\n", signer.Sec())
// Sign message
msg := []byte("hello")
sig, err := signer.Sign(msg)
if err != nil {
panic(err)
}
// Verify signature
valid, err := signer.Verify(msg, sig)
if err != nil || !valid {
panic("invalid signature")
}
// ECDH for encryption
recipientPub := []byte{/* 32 bytes */}
sharedSecret, err := signer.ECDH(recipientPub)
if err != nil {
panic(err)
}
fmt.Printf("Shared secret: %x\n", sharedSecret)
// Wipe keys when done
defer signer.Zero()
}
Loading Existing Keys
package main
import (
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
signer, _ := p8k.New()
// Load from hex secret key
secHex := "..."
secBytes := hex.Dec(secHex)
if err := signer.InitSec(secBytes); err != nil {
panic(err)
}
// Public key is automatically derived
fmt.Printf("Loaded pubkey: %x\n", signer.Pub())
}
Schnorr Signatures
The schnorr package provides low-level signature operations.
package main
import (
"git.mleku.dev/mleku/nostr/crypto/ec/schnorr"
)
func main() {
// Parse public key
pubBytes, err := schnorr.ParseBytes("hex-pubkey")
if err != nil {
panic(err)
}
// Verify signature
msg := []byte("message hash")
sigBytes := []byte{/* 64 bytes */}
if !schnorr.Verify(pubBytes, msg, sigBytes) {
panic("invalid signature")
}
}
Encryption (NIP-04 and NIP-44)
package main
import (
"git.mleku.dev/mleku/nostr/crypto/encryption"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
// Sender keys
sender, _ := p8k.New()
sender.Generate()
// Recipient keys
recipient, _ := p8k.New()
recipient.Generate()
plaintext := "Secret message"
// NIP-44 encryption (recommended)
ciphertext, err := encryption.Nip44Encrypt(
plaintext,
sender,
recipient.Pub(),
)
if err != nil {
panic(err)
}
// NIP-44 decryption
decrypted, err := encryption.Nip44Decrypt(
ciphertext,
recipient,
sender.Pub(),
)
if err != nil {
panic(err)
}
// NIP-04 encryption (legacy)
ciphertext04, err := encryption.Nip04Encrypt(
plaintext,
sender,
recipient.Pub(),
)
if err != nil {
panic(err)
}
// NIP-04 decryption
decrypted04, err := encryption.Nip04Decrypt(
ciphertext04,
recipient,
sender.Pub(),
)
}
Bech32 Encoding
The bech32encoding package provides npub/nsec/note encoding.
package main
import (
"fmt"
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
)
func main() {
signer, _ := p8k.New()
signer.Generate()
// Encode public key as npub
npub := bech32encoding.PubkeyToNpub(signer.Pub())
fmt.Printf("npub: %s\n", npub)
// Encode secret key as nsec
nsec := bech32encoding.SecToNsec(signer.Sec())
fmt.Printf("nsec: %s\n", nsec)
// Decode npub
pubBytes, err := bech32encoding.NpubToPubkey(npub)
if err != nil {
panic(err)
}
// Decode nsec
secBytes, err := bech32encoding.NsecToSec(nsec)
if err != nil {
panic(err)
}
// Encode event ID as note
eventID := []byte{/* 32 bytes */}
note := bech32encoding.EventIDToNote(eventID)
fmt.Printf("note: %s\n", note)
// Decode note
decodedID, err := bech32encoding.NoteToEventID(note)
if err != nil {
panic(err)
}
}
Protocol Helpers
NIP-42 Authentication
The protocol/auth package provides utilities for NIP-42 authentication.
package main
import (
"time"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
"git.mleku.dev/mleku/nostr/protocol/auth"
)
func main() {
signer, _ := p8k.New()
signer.Generate()
// Create auth event
challenge := []byte("challenge-from-relay")
relayURL := []byte("wss://relay.example.com")
authEvent, err := auth.CreateAuthEvent(challenge, relayURL, signer)
if err != nil {
panic(err)
}
// Validate auth event
valid, err := auth.ValidateAuthEvent(authEvent, challenge, relayURL, time.Minute)
if err != nil || !valid {
panic("invalid auth event")
}
}
Utilities
Buffer Pool
The bufpool package provides efficient buffer pooling for zero-copy operations.
package main
import (
"git.mleku.dev/mleku/nostr/utils/bufpool"
)
func main() {
// Get buffer from pool
buf := bufpool.Get()
defer bufpool.Put(buf)
// Use buffer
buf = append(buf, []byte("data")...)
// Buffer is returned to pool on Put
}
URL Normalization
The normalize package provides URL and message normalization utilities.
package main
import (
"git.mleku.dev/mleku/nostr/utils/normalize"
)
func main() {
// Normalize WebSocket URL
normalized := normalize.URL([]byte("relay.damus.io"))
// Returns: "wss://relay.damus.io"
// HTTP to WebSocket conversion
wsURL := normalize.URL([]byte("https://relay.example.com"))
// Returns: "wss://relay.example.com"
// Port handling
withPort := normalize.URL([]byte("relay.example.com:443"))
// Returns: "wss://relay.example.com"
}
Hex Encoding
The hex package provides SIMD-accelerated hex encoding.
package main
import (
"git.mleku.dev/mleku/nostr/encoders/hex"
)
func main() {
// Encode to hex
data := []byte{0x01, 0x02, 0x03}
hexStr := hex.Enc(data)
// Append to buffer
buf := make([]byte, 0, 64)
buf = hex.EncAppend(buf, data)
// Decode from hex
decoded := hex.Dec("010203")
}
Text Utilities
The text package provides JSON escaping and text processing.
package main
import (
"git.mleku.dev/mleku/nostr/encoders/text"
)
func main() {
// Escape for JSON
raw := []byte(`Hello "world"`)
escaped := text.EscapeJSONString(nil, raw)
// Unescape from JSON
unescaped := text.UnescapeJSONString(nil, escaped)
// Check if needs escaping
if text.NeedsEscape(raw) {
// Handle escaping
}
}
Performance Optimizations
SIMD Acceleration
- SHA256: Uses
minio/sha256-simdfor hardware-accelerated hashing - Hex Encoding: Uses
templexxx/xhexfor SIMD hex encoding
Zero-Copy Operations
- Buffer pooling via
bufpoolpackage - Reusable byte slices in all encoders
Marshal(dst []byte)methods append to existing buffers
Memory Management
// Efficient encoding pattern
buf := bufpool.Get()
defer bufpool.Put(buf)
buf = event.Marshal(buf) // Append to buffer
buf = filter.Marshal(buf) // Append more
Binary Encoding
For maximum performance, use binary encoding instead of JSON:
// Binary encoding (more compact, faster)
var buf bytes.Buffer
ev.MarshalBinary(&buf)
// Or to bytes
binBytes := ev.MarshalBinaryToBytes(nil)
Testing
Run tests with:
go test ./...
Run benchmarks:
go test -bench=. -benchmem ./encoders/event
go test -bench=. -benchmem ./encoders/filter
Dependencies
Required
github.com/minio/sha256-simd- SIMD-accelerated SHA256github.com/templexxx/xhex- SIMD hex encodinggithub.com/ebitengine/purego- CGO-free library loadinggithub.com/gorilla/websocket- WebSocket implementationlol.mleku.dev- Logging and error handlinggolang.org/x/crypto- Cryptographic primitivesgolang.org/x/exp- Experimental packages (constraints)lukechampine.com/frand- Fast random number generationgithub.com/puzpuzpuz/xsync/v3- Concurrent data structures
System Requirements
libsecp256k1.sois embedded in the p8k package for Linux- Go 1.25.3 or later
Architecture
Package Structure
git.mleku.dev/mleku/nostr/
├── crypto/ # Cryptographic operations
│ ├── ec/ # Elliptic curve crypto
│ │ ├── schnorr/ # Schnorr signatures
│ │ ├── secp256k1/ # secp256k1 curve
│ │ ├── bech32/ # Bech32 encoding
│ │ ├── musig2/ # MuSig2 multi-sig
│ │ ├── base58/ # Base58 encoding
│ │ ├── ecdsa/ # ECDSA signatures
│ │ ├── taproot/ # Taproot utilities
│ │ └── ...
│ ├── encryption/ # NIP-04/NIP-44
│ ├── keys/ # Key generation and conversion
│ └── p8k/ # Purego secp256k1
├── encoders/ # Nostr protocol encoders
│ ├── event/ # Event encoding
│ ├── filter/ # Filter encoding
│ ├── tag/ # Tag encoding
│ ├── kind/ # Kind constants
│ ├── envelopes/ # WebSocket envelopes
│ ├── hex/ # Hex encoding
│ ├── text/ # Text utilities
│ ├── bech32encoding/ # Bech32 encoding
│ ├── ints/ # Integer encoding
│ ├── reason/ # Reason codes
│ ├── timestamp/ # Timestamp handling
│ └── varint/ # Varint encoding
├── httpauth/ # NIP-98 HTTP auth
├── interfaces/ # Abstract interfaces
│ ├── signer/ # Signer interface
│ │ └── p8k/ # P8K implementation
│ └── codec/ # Codec interfaces
├── protocol/ # Protocol helpers
│ └── auth/ # NIP-42 auth
├── relayinfo/ # NIP-11 relay info
├── utils/ # Utility packages
│ ├── bufpool/ # Buffer pooling
│ ├── normalize/ # URL normalization
│ ├── constraints/ # Type constraints
│ ├── number/ # Number utilities
│ ├── pointers/ # Pointer utilities
│ ├── units/ # Size units
│ └── values/ # Value utilities
└── ws/ # WebSocket client
Design Principles
- Zero-Copy: All
Marshalmethods acceptdst []byteto append to existing buffers - Buffer Pooling: Use
bufpool.Get/Putfor temporary buffers - Pure Go: No CGO, uses purego for C library interop
- Performance: SIMD acceleration where available
- Context-Based: Lifecycle management with context.Context
- Correctness: Extensive test coverage
Complete Examples
Full Relay Client Example
package main
import (
"context"
"fmt"
"time"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
"git.mleku.dev/mleku/nostr/ws"
)
func main() {
ctx := context.Background()
// Initialize keys
signer, _ := p8k.New()
signer.Generate()
// Connect to relay
relay, err := ws.RelayConnect(ctx, "wss://relay.damus.io")
if err != nil {
panic(err)
}
defer relay.Close()
// Create and publish event
ev := event.New()
ev.Kind = kind.TextNote.K
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("Hello Nostr from mleku/nostr!")
ev.Sign(signer)
// Publish event
err = relay.Publish(ctx, ev)
if err != nil {
panic(err)
}
fmt.Printf("Published event: %x\n", ev.ID)
// Subscribe to events
f := filter.New()
f.Kinds = kind.NewS(kind.TextNote)
limit := 10
f.Limit = &limit
sub, err := relay.Subscribe(ctx, filter.NewS(f))
if err != nil {
panic(err)
}
// Process events
for event := range sub.Events {
fmt.Printf("Event: %s\n", string(event.Content))
}
}
Contributing
Contributions are welcome! Please ensure:
- All tests pass:
go test ./... - Code is formatted:
go fmt ./... - Benchmarks show no regressions
- Documentation is updated
License
[Insert license information]
Credits
This library is extracted from the ORLY relay implementation, designed for high-performance Nostr protocol handling.
Key Technologies
- Cryptography: Uses Bitcoin Core's
libsecp256k1via purego - SIMD: MinIO's SHA256 and templexxx's hex encoder
- Logging: lol.mleku.dev logging framework
- WebSocket: Gorilla WebSocket