Go to file

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.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

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-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.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

  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.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:

  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

Description
a simple, highly optimized set of encoders for nostr events, filters, and associated helpers. includes AVX SIMD accelerated hex encoding, matching functions for filters, and API messages (envelopes), bech32 encoded entities, as well as a tested and concurrrent safe websocket client library with subscription handling that follows nostr convention, and the cryptographic operations required, signing, verification, ecdh and x-only pubkey derivation
Readme 14 MiB
Languages
Go 97.8%
Python 1.4%
Shell 0.4%
Makefile 0.4%