Files
nostr/README.md

38 KiB

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