clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / protocol / nostr / event.go
event.go 7.3K
1package nostr23import (4 "bytes"5 "crypto/rand"6 "crypto/sha256"7 "encoding/hex"8 "encoding/json"9 "errors"10 "fmt"11 "strconv"12 "strings"1314 "fiatjaf.com/nostr"15 "fiatjaf.com/nostr/nip13"16 "github.com/btcsuite/btcd/btcec/v2"17 "github.com/btcsuite/btcd/btcec/v2/schnorr"18)1920type Event struct {21 ID string `json:"id"`22 PubKey string `json:"pubkey"`23 CreatedAt int64 `json:"created_at"`24 Kind int `json:"kind"`25 Tags [][]string `json:"tags"`26 Content string `json:"content"`27 Sig string `json:"sig"`28}2930// Validate checks the wire shape, canonical ID and Schnorr signature of e.31// Kind-specific checks are applied by the protocol gate that owns the kind.32func Validate(e Event) error {33 if !hex64(e.ID) {34 return errors.New("invalid: bad id")35 }36 if !hex64(e.PubKey) {37 return errors.New("invalid: bad pubkey")38 }39 if len(e.Sig) != 128 || !isHex(e.Sig) {40 return errors.New("invalid: bad sig")41 }42 if e.Kind < 0 || e.Kind > 65535 {43 return errors.New("invalid: kind out of range")44 }45 if e.CreatedAt < 0 {46 return errors.New("invalid: bad created_at")47 }48 if e.Tags == nil {49 return errors.New("invalid: tags must be a list")50 }51 for _, tag := range e.Tags {52 if len(tag) == 0 {53 return errors.New("invalid: bad tag")54 }55 }56 serialized, err := serializeForID(e)57 if err != nil {58 return errors.New("invalid: cannot serialize event")59 }60 sum := sha256.Sum256(serialized)61 if e.ID != hex.EncodeToString(sum[:]) || !verifySignature(e, sum[:]) {62 return errors.New("invalid: id or signature does not match content")63 }64 return nil65}6667// Parse decodes and validates one complete JSON event object.68func Parse(raw []byte) (Event, error) {69 if err := requireFields(raw); err != nil {70 return Event{}, err71 }72 var e Event73 if err := json.Unmarshal(raw, &e); err != nil {74 return Event{}, fmt.Errorf("invalid event JSON: %w", err)75 }76 if err := Validate(e); err != nil {77 return Event{}, err78 }79 return e, nil80}8182// Canonical encodes e as the JSON representation used on relay boundaries.83func Canonical(e Event) ([]byte, error) {84 var out bytes.Buffer85 encoder := json.NewEncoder(&out)86 encoder.SetEscapeHTML(false)87 if err := encoder.Encode(e); err != nil {88 return nil, err89 }90 return bytes.TrimSuffix(out.Bytes(), []byte{'\n'}), nil91}9293// Tag returns the first value for name, or an empty string when the tag is94// absent or has no value.95func Tag(e Event, name string) string {96 for _, tag := range e.Tags {97 if len(tag) > 0 && tag[0] == name && len(tag) > 1 {98 return tag[1]99 }100 }101 return ""102}103104// TagValues returns every first value belonging to tags named name.105func TagValues(e Event, name string) []string {106 values := make([]string, 0)107 for _, tag := range e.Tags {108 if len(tag) > 1 && tag[0] == name {109 values = append(values, tag[1])110 }111 }112 return values113}114115// Expiration returns the positive Unix timestamp in the expiration tag.116// Malformed, missing and nonpositive values return zero.117func Expiration(e Event) int64 {118 v, err := strconv.ParseInt(Tag(e, "expiration"), 10, 64)119 if err != nil || v <= 0 {120 return 0121 }122 return v123}124125// Difficulty returns the NIP-13 proof-of-work difficulty encoded by id.126func Difficulty(id string) int {127 parsed, err := nostr.IDFromHex(id)128 if err != nil {129 return 0130 }131 return nip13.Difficulty(parsed)132}133134// CommittedDifficulty returns the target in a nonce tag's third field.135func CommittedDifficulty(e Event) int {136 for _, tag := range e.Tags {137 if len(tag) > 2 && tag[0] == "nonce" {138 n, _ := strconv.Atoi(tag[2])139 return n140 }141 }142 return 0143}144145// Sign derives the public key, ID and Schnorr signature for e from secretHex.146// A nil tag slice is encoded as an empty tag list before signing.147func Sign(e *Event, secretHex string) error {148 secret, err := decodeSecret(secretHex)149 if err != nil {150 return err151 }152 if e.Tags == nil {153 e.Tags = [][]string{}154 }155 key, pubkey := btcec.PrivKeyFromBytes(secret[:])156 e.PubKey = hex.EncodeToString(schnorr.SerializePubKey(pubkey))157 serialized, err := serializeForID(*e)158 if err != nil {159 return fmt.Errorf("serialize event: %w", err)160 }161 sum := sha256.Sum256(serialized)162 signature, err := schnorr.Sign(key, sum[:])163 if err != nil {164 return fmt.Errorf("sign event: %w", err)165 }166 e.ID = hex.EncodeToString(sum[:])167 e.Sig = hex.EncodeToString(signature.Serialize())168 return nil169}170171// GenerateKey creates a random nonzero 32-byte private key in hexadecimal.172func GenerateKey() (string, error) {173 for {174 key := make([]byte, 32)175 if _, err := rand.Read(key); err != nil {176 return "", fmt.Errorf("generate private key: %w", err)177 }178 if !allZero(key) {179 return hex.EncodeToString(key), nil180 }181 }182}183184// PublicKey derives the x-only Schnorr public key for secretHex.185func PublicKey(secretHex string) (string, error) {186 secret, err := decodeSecret(secretHex)187 if err != nil {188 return "", err189 }190 _, pubkey := btcec.PrivKeyFromBytes(secret[:])191 return hex.EncodeToString(schnorr.SerializePubKey(pubkey)), nil192}193194// IsEphemeral reports whether k is in Nostr's ephemeral kind range.195func IsEphemeral(k int) bool { return k >= 20000 && k < 30000 }196197// IsReplaceable reports whether k is a replaceable event kind.198func IsReplaceable(k int) bool { return k == 0 || k == 3 || k >= 10000 && k < 20000 }199200// IsAddressable reports whether k is an addressable event kind.201func IsAddressable(k int) bool { return k >= 30000 && k < 40000 }202203func hex64(s string) bool { return len(s) == 64 && isHex(s) }204func isHex(s string) bool {205 for _, char := range s {206 if char >= '0' && char <= '9' || char >= 'a' && char <= 'f' {207 continue208 }209 return false210 }211 return true212}213214func serializeForID(e Event) ([]byte, error) {215 value := []any{0, e.PubKey, e.CreatedAt, e.Kind, e.Tags, e.Content}216 var out bytes.Buffer217 encoder := json.NewEncoder(&out)218 encoder.SetEscapeHTML(false)219 if err := encoder.Encode(value); err != nil {220 return nil, err221 }222 serialized := bytes.TrimSuffix(out.Bytes(), []byte{'\n'})223 serialized = bytes.ReplaceAll(serialized, []byte(`\u2028`), []byte("\u2028"))224 serialized = bytes.ReplaceAll(serialized, []byte(`\u2029`), []byte("\u2029"))225 return serialized, nil226}227228func verifySignature(e Event, hash []byte) bool {229 pubkey, err := hex.DecodeString(e.PubKey)230 if err != nil {231 return false232 }233 key, err := schnorr.ParsePubKey(pubkey)234 if err != nil {235 return false236 }237 signature, err := hex.DecodeString(e.Sig)238 if err != nil {239 return false240 }241 sig, err := schnorr.ParseSignature(signature)242 return err == nil && sig.Verify(hash, key)243}244245func decodeSecret(value string) ([32]byte, error) {246 var secret [32]byte247 if len(value) != 64 {248 return secret, errors.New("invalid secret key: expected 64 hex characters")249 }250 if _, err := hex.Decode(secret[:], []byte(strings.ToLower(value))); err != nil {251 return secret, fmt.Errorf("invalid secret key: %w", err)252 }253 if allZero(secret[:]) {254 return secret, errors.New("invalid secret key")255 }256 return secret, nil257}258259func allZero(value []byte) bool {260 for _, item := range value {261 if item != 0 {262 return false263 }264 }265 return true266}267268func requireFields(raw []byte) error {269 var fields map[string]json.RawMessage270 if err := json.Unmarshal(raw, &fields); err != nil {271 return fmt.Errorf("invalid event JSON: %w", err)272 }273 for _, field := range []string{"id", "pubkey", "created_at", "kind", "tags", "content", "sig"} {274 if _, ok := fields[field]; !ok {275 return fmt.Errorf("invalid: missing %s", field)276 }277 }278 return nil279}