| 1 | package nostr |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "crypto/rand" |
| 6 | "crypto/sha256" |
| 7 | "encoding/hex" |
| 8 | "encoding/json" |
| 9 | "errors" |
| 10 | "fmt" |
| 11 | "strconv" |
| 12 | "strings" |
| 13 | |
| 14 | "fiatjaf.com/nostr" |
| 15 | "fiatjaf.com/nostr/nip13" |
| 16 | "github.com/btcsuite/btcd/btcec/v2" |
| 17 | "github.com/btcsuite/btcd/btcec/v2/schnorr" |
| 18 | ) |
| 19 | |
| 20 | type 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 | } |
| 29 | |
| 30 | // 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. |
| 32 | func 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 nil |
| 65 | } |
| 66 | |
| 67 | // Parse decodes and validates one complete JSON event object. |
| 68 | func Parse(raw []byte) (Event, error) { |
| 69 | if err := requireFields(raw); err != nil { |
| 70 | return Event{}, err |
| 71 | } |
| 72 | var e Event |
| 73 | 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{}, err |
| 78 | } |
| 79 | return e, nil |
| 80 | } |
| 81 | |
| 82 | // Canonical encodes e as the JSON representation used on relay boundaries. |
| 83 | func Canonical(e Event) ([]byte, error) { |
| 84 | var out bytes.Buffer |
| 85 | encoder := json.NewEncoder(&out) |
| 86 | encoder.SetEscapeHTML(false) |
| 87 | if err := encoder.Encode(e); err != nil { |
| 88 | return nil, err |
| 89 | } |
| 90 | return bytes.TrimSuffix(out.Bytes(), []byte{'\n' }), nil |
| 91 | } |
| 92 | |
| 93 | // Tag returns the first value for name, or an empty string when the tag is |
| 94 | // absent or has no value. |
| 95 | func 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 | } |
| 103 | |
| 104 | // TagValues returns every first value belonging to tags named name. |
| 105 | func 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 values |
| 113 | } |
| 114 | |
| 115 | // Expiration returns the positive Unix timestamp in the expiration tag. |
| 116 | // Malformed, missing and nonpositive values return zero. |
| 117 | func Expiration(e Event) int64 { |
| 118 | v, err := strconv.ParseInt(Tag(e, "expiration" ), 10, 64) |
| 119 | if err != nil || v <= 0 { |
| 120 | return 0 |
| 121 | } |
| 122 | return v |
| 123 | } |
| 124 | |
| 125 | // Difficulty returns the NIP-13 proof-of-work difficulty encoded by id. |
| 126 | func Difficulty(id string) int { |
| 127 | parsed, err := nostr.IDFromHex(id) |
| 128 | if err != nil { |
| 129 | return 0 |
| 130 | } |
| 131 | return nip13.Difficulty(parsed) |
| 132 | } |
| 133 | |
| 134 | // CommittedDifficulty returns the target in a nonce tag's third field. |
| 135 | func 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 n |
| 140 | } |
| 141 | } |
| 142 | return 0 |
| 143 | } |
| 144 | |
| 145 | // 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. |
| 147 | func Sign(e *Event, secretHex string) error { |
| 148 | secret, err := decodeSecret(secretHex) |
| 149 | if err != nil { |
| 150 | return err |
| 151 | } |
| 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 nil |
| 169 | } |
| 170 | |
| 171 | // GenerateKey creates a random nonzero 32-byte private key in hexadecimal. |
| 172 | func 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), nil |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | // PublicKey derives the x-only Schnorr public key for secretHex. |
| 185 | func PublicKey(secretHex string) (string, error) { |
| 186 | secret, err := decodeSecret(secretHex) |
| 187 | if err != nil { |
| 188 | return "" , err |
| 189 | } |
| 190 | _, pubkey := btcec.PrivKeyFromBytes(secret[:]) |
| 191 | return hex.EncodeToString(schnorr.SerializePubKey(pubkey)), nil |
| 192 | } |
| 193 | |
| 194 | // IsEphemeral reports whether k is in Nostr's ephemeral kind range. |
| 195 | func IsEphemeral(k int) bool { return k >= 20000 && k < 30000 } |
| 196 | |
| 197 | // IsReplaceable reports whether k is a replaceable event kind. |
| 198 | func IsReplaceable(k int) bool { return k == 0 || k == 3 || k >= 10000 && k < 20000 } |
| 199 | |
| 200 | // IsAddressable reports whether k is an addressable event kind. |
| 201 | func IsAddressable(k int) bool { return k >= 30000 && k < 40000 } |
| 202 | |
| 203 | func hex64(s string) bool { return len(s) == 64 && isHex(s) } |
| 204 | func isHex(s string) bool { |
| 205 | for _, char := range s { |
| 206 | if char >= '0' && char <= '9' || char >= 'a' && char <= 'f' { |
| 207 | continue |
| 208 | } |
| 209 | return false |
| 210 | } |
| 211 | return true |
| 212 | } |
| 213 | |
| 214 | func serializeForID(e Event) ([]byte, error) { |
| 215 | value := []any{0, e.PubKey, e.CreatedAt, e.Kind, e.Tags, e.Content} |
| 216 | var out bytes.Buffer |
| 217 | encoder := json.NewEncoder(&out) |
| 218 | encoder.SetEscapeHTML(false) |
| 219 | if err := encoder.Encode(value); err != nil { |
| 220 | return nil, err |
| 221 | } |
| 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, nil |
| 226 | } |
| 227 | |
| 228 | func verifySignature(e Event, hash []byte) bool { |
| 229 | pubkey, err := hex.DecodeString(e.PubKey) |
| 230 | if err != nil { |
| 231 | return false |
| 232 | } |
| 233 | key, err := schnorr.ParsePubKey(pubkey) |
| 234 | if err != nil { |
| 235 | return false |
| 236 | } |
| 237 | signature, err := hex.DecodeString(e.Sig) |
| 238 | if err != nil { |
| 239 | return false |
| 240 | } |
| 241 | sig, err := schnorr.ParseSignature(signature) |
| 242 | return err == nil && sig.Verify(hash, key) |
| 243 | } |
| 244 | |
| 245 | func decodeSecret(value string) ([32]byte, error) { |
| 246 | var secret [32]byte |
| 247 | 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, nil |
| 257 | } |
| 258 | |
| 259 | func allZero(value []byte) bool { |
| 260 | for _, item := range value { |
| 261 | if item != 0 { |
| 262 | return false |
| 263 | } |
| 264 | } |
| 265 | return true |
| 266 | } |
| 267 | |
| 268 | func requireFields(raw []byte) error { |
| 269 | var fields map[string]json.RawMessage |
| 270 | 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 nil |
| 279 | } |