nostr/

directory
v0.65.60 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: AGPL-3.0

README

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/purego to dynamically load libsecp256k1.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.smesh.lol/orly/pkg/nostr

Quick Start

Connect to a Relay and Subscribe
package main

import (
    "context"
    "fmt"

    "git.smesh.lol/orly/pkg/nostr/encoders/filter"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/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

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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/filter"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/event"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/event"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/nostr/encoders/tag"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/crypto/ec/schnorr"
    "git.smesh.lol/orly/pkg/nostr/encoders/event"
    "git.smesh.lol/orly/pkg/nostr/encoders/filter"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/hex"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/envelopes/eventenvelope"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/envelopes/reqenvelope"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/envelopes/okenvelope"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/httpauth"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/hex"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/crypto/encryption"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/encoders/bech32encoding"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
    "git.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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.smesh.lol/orly/pkg/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-simd for hardware-accelerated hashing
  • Hex Encoding: Uses templexxx/xhex for SIMD hex encoding
Zero-Copy Operations
  • Buffer pooling via bufpool package
  • 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 SHA256
  • github.com/templexxx/xhex - SIMD hex encoding
  • github.com/ebitengine/purego - CGO-free library loading
  • github.com/gorilla/websocket - WebSocket implementation
  • lol.mleku.dev - Logging and error handling
  • golang.org/x/crypto - Cryptographic primitives
  • golang.org/x/exp - Experimental packages (constraints)
  • lukechampine.com/frand - Fast random number generation
  • github.com/puzpuzpuz/xsync/v3 - Concurrent data structures
System Requirements
  • libsecp256k1.so is embedded in the p8k package for Linux
  • Go 1.25.3 or later

Architecture

Package Structure
git.smesh.lol/orly/pkg/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
  1. Zero-Copy: All Marshal methods accept dst []byte to append to existing buffers
  2. Buffer Pooling: Use bufpool.Get/Put for temporary buffers
  3. Pure Go: No CGO, uses purego for C library interop
  4. Performance: SIMD acceleration where available
  5. Context-Based: Lifecycle management with context.Context
  6. Correctness: Extensive test coverage

Complete Examples

Full Relay Client Example
package main

import (
    "context"
    "fmt"
    "time"

    "git.smesh.lol/orly/pkg/nostr/encoders/event"
    "git.smesh.lol/orly/pkg/nostr/encoders/filter"
    "git.smesh.lol/orly/pkg/nostr/encoders/kind"
    "git.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
    "git.smesh.lol/orly/pkg/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:

  1. All tests pass: go test ./...
  2. Code is formatted: go fmt ./...
  3. Benchmarks show no regressions
  4. 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 libsecp256k1 via purego
  • SIMD: MinIO's SHA256 and templexxx's hex encoder
  • Logging: lol.mleku.dev logging framework
  • WebSocket: Gorilla WebSocket

See Also

Directories

Path Synopsis
crypto
ec
Package btcec implements support for the elliptic curves needed for bitcoin.
Package btcec implements support for the elliptic curves needed for bitcoin.
ec/base58
Package base58 provides an API for working with modified base58 and Base58Check encodings.
Package base58 provides an API for working with modified base58 and Base58Check encodings.
ec/base58/gen command
ec/bech32
Package bech32 provides a Go implementation of the bech32 format specified in BIP 173.
Package bech32 provides a Go implementation of the bech32 format specified in BIP 173.
ec/chaincfg
Package chaincfg provides basic parameters for bitcoin chain and testnets.
Package chaincfg provides basic parameters for bitcoin chain and testnets.
ec/chainhash
Package chainhash provides abstracted hash functionality.
Package chainhash provides abstracted hash functionality.
ec/ecdsa
Package ecdsa provides secp256k1-optimized ECDSA signing and verification.
Package ecdsa provides secp256k1-optimized ECDSA signing and verification.
ec/musig2
Package musig2 provides an implementation of the musig2 protocol for bitcoin.
Package musig2 provides an implementation of the musig2 protocol for bitcoin.
ec/schnorr
Package schnorr provides custom Schnorr signing and verification via secp256k1.
Package schnorr provides custom Schnorr signing and verification via secp256k1.
ec/secp256k1
Package secp256k1 implements optimized secp256k1 elliptic curve operations in pure Go.
Package secp256k1 implements optimized secp256k1 elliptic curve operations in pure Go.
ec/secp256k1/precomps command
Package main provides a generator for precomputed constants for secp256k1 signatures.
Package main provides a generator for precomputed constants for secp256k1 signatures.
ec/taproot
Package taproot provides a collection of tools for encoding bitcoin taproot addresses.
Package taproot provides a collection of tools for encoding bitcoin taproot addresses.
ec/wire
Package wire contains a set of data structure definitions for the bitcoin blockchain.
Package wire contains a set of data structure definitions for the bitcoin blockchain.
encryption
Package encryption contains the message encryption schemes defined in NIP-04 and NIP-44, used for encrypting the content of nostr messages.
Package encryption contains the message encryption schemes defined in NIP-04 and NIP-44, used for encrypting the content of nostr messages.
keys
Package keys is a set of helpers for generating and converting public/secret keys to hex and back to binary.
Package keys is a set of helpers for generating and converting public/secret keys to hex and back to binary.
encoders
bech32encoding
Package bech32encoding implements NIP-19 entities, which are bech32 encoded data that describes nostr data types.
Package bech32encoding implements NIP-19 entities, which are bech32 encoded data that describes nostr data types.
bech32encoding/pointers
Package pointers is a set of basic nip-19 data types for generating bech32 encoded nostr entities.
Package pointers is a set of basic nip-19 data types for generating bech32 encoded nostr entities.
bech32encoding/tlv
Package tlv implements a simple Type Length Value encoder for nostr NIP-19 bech32 encoded entities.
Package tlv implements a simple Type Length Value encoder for nostr NIP-19 bech32 encoded entities.
envelopes
Package envelopes provides common functions for marshaling and identifying nostr envelopes (JSON arrays containing protocol messages).
Package envelopes provides common functions for marshaling and identifying nostr envelopes (JSON arrays containing protocol messages).
envelopes/authenvelope
Package authenvelope defines the auth challenge (relay message) and response (client message) of the NIP-42 authentication protocol.
Package authenvelope defines the auth challenge (relay message) and response (client message) of the NIP-42 authentication protocol.
envelopes/closedenvelope
Package closedenvelope defines the nostr message type CLOSED which is sent from a relay to indicate the relay-side termination of a subscription or the demand for authentication associated with a subscription.
Package closedenvelope defines the nostr message type CLOSED which is sent from a relay to indicate the relay-side termination of a subscription or the demand for authentication associated with a subscription.
envelopes/closeenvelope
Package closeenvelope provides the encoder for the client message CLOSE which is a request to terminate a subscription.
Package closeenvelope provides the encoder for the client message CLOSE which is a request to terminate a subscription.
envelopes/countenvelope
Package countenvelope is an encoder for the COUNT request (client) and response (relay) message types.
Package countenvelope is an encoder for the COUNT request (client) and response (relay) message types.
envelopes/eoseenvelope
Package eoseenvelope provides an encoder for the EOSE (End Of Stored Events) event that signifies that a REQ has found all stored events and from here on the request morphs into a subscription, until the limit, if requested, or until CLOSE or CLOSED.
Package eoseenvelope provides an encoder for the EOSE (End Of Stored Events) event that signifies that a REQ has found all stored events and from here on the request morphs into a subscription, until the limit, if requested, or until CLOSE or CLOSED.
envelopes/eventenvelope
Package eventenvelope is a codec for the event Submission request EVENT envelope (client) and event Result (to a REQ) from a relay.
Package eventenvelope is a codec for the event Submission request EVENT envelope (client) and event Result (to a REQ) from a relay.
envelopes/messages
Package messages is a collection of example/common messages and machine-readable prefixes to use with OK and CLOSED envelopes.
Package messages is a collection of example/common messages and machine-readable prefixes to use with OK and CLOSED envelopes.
envelopes/noticeenvelope
Package noticeenvelope is a codec for the NOTICE envelope, which is used to serve (mostly ignored) messages that are supposed to be shown to a user in the client.
Package noticeenvelope is a codec for the NOTICE envelope, which is used to serve (mostly ignored) messages that are supposed to be shown to a user in the client.
envelopes/okenvelope
Package okenvelope is a codec for the OK message, which is an acknowledgement for an EVENT eventenvelope.Submission, containing true/false and if false a message with a machine readable error type as found in the messages package.
Package okenvelope is a codec for the OK message, which is an acknowledgement for an EVENT eventenvelope.Submission, containing true/false and if false a message with a machine readable error type as found in the messages package.
envelopes/reqenvelope
Package reqenvelope is a message from a client to a relay containing a subscription identifier and an array of filters to search for events.
Package reqenvelope is a message from a client to a relay containing a subscription identifier and an array of filters to search for events.
event/examples
Package examples is an embedded jsonl format of a collection of events intended to be used to test an event codec.
Package examples is an embedded jsonl format of a collection of events intended to be used to test an event codec.
hex
Package hex is a set of aliases and helpers for using the templexxx SIMD hex encoder.
Package hex is a set of aliases and helpers for using the templexxx SIMD hex encoder.
ints
Package ints is an optimised encoder for decimal numbers in ASCII format, that simplifies and accelerates encoding and decoding decimal strings.
Package ints is an optimised encoder for decimal numbers in ASCII format, that simplifies and accelerates encoding and decoding decimal strings.
ints/gen command
Package main is a generator for the base10000 (4 digit) encoding of the ints library.
Package main is a generator for the base10000 (4 digit) encoding of the ints library.
kind
Package kind includes a type for convenient handling of event kinds, and a kind database with reverse lookup for human-readable information about event kinds.
Package kind includes a type for convenient handling of event kinds, and a kind database with reverse lookup for human-readable information about event kinds.
tag
Package tag provides an implementation of a nostr tag list, an array of strings with a usually single letter first "key" field, including methods to compare, marshal/unmarshal and access elements with their proper semantics.
Package tag provides an implementation of a nostr tag list, an array of strings with a usually single letter first "key" field, including methods to compare, marshal/unmarshal and access elements with their proper semantics.
tag/atag
Package atag implements a special, optimized handling for keeping a tags (address) in a more memory efficient form while working with these tags.
Package atag implements a special, optimized handling for keeping a tags (address) in a more memory efficient form while working with these tags.
timestamp
Package timestamp is a set of helpers for working with timestamps including encoding and conversion to various integer forms, from time.Time and varints.
Package timestamp is a set of helpers for working with timestamps including encoding and conversion to various integer forms, from time.Time and varints.
varint
Package varint is a variable integer encoding that works in reverse compared to the stdlib binary Varint.
Package varint is a variable integer encoding that works in reverse compared to the stdlib binary Varint.
Package httpauth provides helpers and encoders for nostr NIP-98 HTTP authentication header messages and a new JWT authentication message and delegation event kind 13004 that enables time limited expiring delegations of authentication (as with NIP-42 auth) for the HTTP API.
Package httpauth provides helpers and encoders for nostr NIP-98 HTTP authentication header messages and a new JWT authentication message and delegation event kind 13004 that enables time limited expiring delegations of authentication (as with NIP-42 auth) for the HTTP API.
interfaces
signer
Package signer defines server for management of signatures, used to abstract the signature algorithm from the usage.
Package signer defines server for management of signatures, used to abstract the signature algorithm from the usage.
signer/p8k
Package p8k provides a signer.I implementation using the pure Go p256k1.mleku.dev library with BMI2-accelerated assembly on AMD64.
Package p8k provides a signer.I implementation using the pure Go p256k1.mleku.dev library with BMI2-accelerated assembly on AMD64.
Package negentropy implements NIP-77 negentropy-based set reconciliation.
Package negentropy implements NIP-77 negentropy-based set reconciliation.
protocol
marmot
Package marmot implements the Marmot protocol (MLS-based E2E encrypted messaging) for Nostr.
Package marmot implements the Marmot protocol (MLS-based E2E encrypted messaging) for Nostr.
Package types provides fixed-size cryptographic types for Nostr.
Package types provides fixed-size cryptographic types for Nostr.
normalize
Package normalize is a set of tools for cleaning up URL s and formatting nostr OK and CLOSED messages.
Package normalize is a set of tools for cleaning up URL s and formatting nostr OK and CLOSED messages.
number
Package number implements a simple number list, used with relayinfo package for NIP support lists.
Package number implements a simple number list, used with relayinfo package for NIP support lists.
units
Package units is a convenient set of names designating data sizes in bytes using common ISO names (base 10).
Package units is a convenient set of names designating data sizes in bytes using common ISO names (base 10).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL