event.go raw
1 package event
2
3 import (
4 "fmt"
5 "io"
6
7 "next.orly.dev/pkg/nostr/crypto/ec/schnorr"
8 "next.orly.dev/pkg/nostr/encoders/ints"
9 "next.orly.dev/pkg/nostr/encoders/kind"
10 "next.orly.dev/pkg/nostr/encoders/tag"
11 "next.orly.dev/pkg/nostr/encoders/text"
12 "next.orly.dev/pkg/nostr/utils"
13 "github.com/minio/sha256-simd"
14 "github.com/templexxx/xhex"
15 "next.orly.dev/pkg/lol/chk"
16 "next.orly.dev/pkg/lol/errorf"
17 )
18
19 // E is the primary datatype of nostr. This is the form of the structure that
20 // defines its JSON string-based format.
21 //
22 // WARNING: DO NOT use json.Marshal with this type because it will not properly
23 // encode <, >, and & characters due to legacy bullcrap in the encoding/json
24 // library. Either call MarshalJSON directly or use a json.Encoder with html
25 // escaping disabled.
26 type E struct {
27
28 // ID is the SHA256 hash of the canonical encoding of the event in binary
29 // format
30 ID []byte
31
32 // Pubkey is the public key of the event creator in binary format
33 Pubkey []byte
34
35 // CreatedAt is the UNIX timestamp of the event according to the event
36 // creator (never trust a timestamp!)
37 CreatedAt int64
38
39 // Kind is the nostr protocol code for the type of event. See kind.T
40 Kind uint16
41
42 // Tags are a list of tags, which are a list of strings usually structured
43 // as a 3-layer scheme indicating specific features of an event.
44 Tags *tag.S
45
46 // Content is an arbitrary string that can contain anything, but usually
47 // conforming to a specification relating to the Kind and the Tags.
48 Content []byte
49
50 // Sig is the signature on the ID hash that validates as coming from the
51 // Pubkey in binary format.
52 Sig []byte
53 }
54
55 var (
56 jId = []byte("id")
57 jPubkey = []byte("pubkey")
58 jCreatedAt = []byte("created_at")
59 jKind = []byte("kind")
60 jTags = []byte("tags")
61 jContent = []byte("content")
62 jSig = []byte("sig")
63 )
64
65 // New returns a new event.E.
66 func New() *E {
67 return &E{}
68 }
69
70 // Free nils all of the fields to hint to the GC that the event.E can be freed.
71 func (ev *E) Free() {
72 ev.ID = nil
73 ev.Pubkey = nil
74 ev.Tags = nil
75 ev.Content = nil
76 ev.Sig = nil
77 }
78
79 // Clone creates a deep copy of the event with independent memory allocations.
80 // The clone does not use bufpool, ensuring it has a separate lifetime from
81 // the original event. This prevents corruption when the original is freed
82 // while the clone is still in use (e.g., in asynchronous delivery).
83 func (ev *E) Clone() *E {
84 clone := &E{
85 CreatedAt: ev.CreatedAt,
86 Kind: ev.Kind,
87 }
88
89 // Deep copy all byte slices with independent memory
90 if ev.ID != nil {
91 clone.ID = make([]byte, len(ev.ID))
92 copy(clone.ID, ev.ID)
93 }
94 if ev.Pubkey != nil {
95 clone.Pubkey = make([]byte, len(ev.Pubkey))
96 copy(clone.Pubkey, ev.Pubkey)
97 }
98 if ev.Content != nil {
99 clone.Content = make([]byte, len(ev.Content))
100 copy(clone.Content, ev.Content)
101 }
102 if ev.Sig != nil {
103 clone.Sig = make([]byte, len(ev.Sig))
104 copy(clone.Sig, ev.Sig)
105 }
106
107 // Deep copy tags
108 if ev.Tags != nil {
109 clone.Tags = tag.NewS()
110 for _, tg := range *ev.Tags {
111 if tg != nil {
112 // Create new tag with deep-copied elements
113 newTag := tag.NewWithCap(len(tg.T))
114 for _, element := range tg.T {
115 newElement := make([]byte, len(element))
116 copy(newElement, element)
117 newTag.T = append(newTag.T, newElement)
118 }
119 clone.Tags.Append(newTag)
120 }
121 }
122 }
123
124 return clone
125 }
126
127 // EstimateSize returns a size for the event that allows for worst case scenario
128 // expansion of the escaped content and tags.
129 func (ev *E) EstimateSize() (size int) {
130 size = len(ev.ID)*2 + len(ev.Pubkey)*2 + len(ev.Sig)*2 + len(ev.Content)*2
131 if ev.Tags == nil {
132 return
133 }
134 for _, v := range *ev.Tags {
135 for _, w := range (*v).T {
136 size += len(w) * 2
137 }
138 }
139 return
140 }
141
142 func (ev *E) Marshal(dst []byte) (b []byte) {
143 b = dst
144 // Pre-allocate buffer if nil to reduce reallocations
145 if b == nil {
146 estimatedSize := ev.EstimateSize()
147 // Add overhead for JSON structure (keys, quotes, commas, etc.)
148 estimatedSize += 100
149 b = make([]byte, 0, estimatedSize)
150 }
151 b = append(b, '{')
152 b = append(b, '"')
153 b = append(b, jId...)
154 b = append(b, `":"`...)
155 // Pre-allocate hex encoding space
156 hexStart := len(b)
157 b = append(b, make([]byte, 2*sha256.Size)...)
158 xhex.Encode(b[hexStart:], ev.ID)
159 b = append(b, `","`...)
160 b = append(b, jPubkey...)
161 b = append(b, `":"`...)
162 hexStart = len(b)
163 b = append(b, make([]byte, 2*schnorr.PubKeyBytesLen)...)
164 xhex.Encode(b[hexStart:], ev.Pubkey)
165 b = append(b, `","`...)
166 b = append(b, jCreatedAt...)
167 b = append(b, `":`...)
168 b = ints.New(ev.CreatedAt).Marshal(b)
169 b = append(b, `,"`...)
170 b = append(b, jKind...)
171 b = append(b, `":`...)
172 b = ints.New(ev.Kind).Marshal(b)
173 b = append(b, `,"`...)
174 b = append(b, jTags...)
175 b = append(b, `":`...)
176 if ev.Tags != nil {
177 b = ev.Tags.Marshal(b)
178 } else {
179 // Emit empty array for nil tags to keep JSON valid
180 b = append(b, '[', ']')
181 }
182 b = append(b, `,"`...)
183 b = append(b, jContent...)
184 b = append(b, `":"`...)
185 b = text.NostrEscape(b, ev.Content)
186 b = append(b, `","`...)
187 b = append(b, jSig...)
188 b = append(b, `":"`...)
189 hexStart = len(b)
190 b = append(b, make([]byte, 2*schnorr.SignatureSize)...)
191 xhex.Encode(b[hexStart:], ev.Sig)
192 b = append(b, `"}`...)
193 return
194 }
195
196 // MarshalJSON marshals an event.E into a JSON byte string.
197 //
198 // WARNING: if json.Marshal is called in the hopes of invoking this function on
199 // an event, if it has <, > or * in the content or tags they are escaped into
200 // unicode escapes and break the event ID. Call this function directly in order
201 // to bypass this issue.
202 func (ev *E) MarshalJSON() (b []byte, err error) {
203 b = ev.Marshal(nil)
204 return
205 }
206
207 func (ev *E) Serialize() (b []byte) {
208 b = ev.Marshal(nil)
209 return
210 }
211
212 // Unmarshal unmarshalls a JSON string into an event.E.
213 func (ev *E) Unmarshal(b []byte) (rem []byte, err error) {
214 key := make([]byte, 0, 9)
215 for ; len(b) > 0; b = b[1:] {
216 // Skip whitespace
217 if isWhitespace(b[0]) {
218 continue
219 }
220 if b[0] == '{' {
221 b = b[1:]
222 goto BetweenKeys
223 }
224 }
225 goto eof
226 BetweenKeys:
227 for ; len(b) > 0; b = b[1:] {
228 // Skip whitespace
229 if isWhitespace(b[0]) {
230 continue
231 }
232 if b[0] == '"' {
233 b = b[1:]
234 goto InKey
235 }
236 }
237 goto eof
238 InKey:
239 for ; len(b) > 0; b = b[1:] {
240 if b[0] == '"' {
241 b = b[1:]
242 goto InKV
243 }
244 key = append(key, b[0])
245 }
246 goto eof
247 InKV:
248 for ; len(b) > 0; b = b[1:] {
249 // Skip whitespace
250 if isWhitespace(b[0]) {
251 continue
252 }
253 if b[0] == ':' {
254 b = b[1:]
255 goto InVal
256 }
257 }
258 goto eof
259 InVal:
260 // Skip whitespace before value
261 for len(b) > 0 && isWhitespace(b[0]) {
262 b = b[1:]
263 }
264 switch key[0] {
265 case jId[0]:
266 if !utils.FastEqual(jId, key) {
267 goto invalid
268 }
269 var id []byte
270 if id, b, err = text.UnmarshalHex(b); chk.E(err) {
271 return
272 }
273 if len(id) != sha256.Size {
274 err = errorf.E(
275 "invalid Subscription, require %d got %d", sha256.Size,
276 len(id),
277 )
278 return
279 }
280 ev.ID = id
281 goto BetweenKV
282 case jPubkey[0]:
283 if !utils.FastEqual(jPubkey, key) {
284 goto invalid
285 }
286 var pk []byte
287 if pk, b, err = text.UnmarshalHex(b); chk.E(err) {
288 return
289 }
290 if len(pk) != schnorr.PubKeyBytesLen {
291 err = errorf.E(
292 "invalid pubkey, require %d got %d",
293 schnorr.PubKeyBytesLen, len(pk),
294 )
295 return
296 }
297 ev.Pubkey = pk
298 goto BetweenKV
299 case jKind[0]:
300 if !utils.FastEqual(jKind, key) {
301 goto invalid
302 }
303 k := kind.New(0)
304 if b, err = k.Unmarshal(b); chk.E(err) {
305 return
306 }
307 ev.Kind = k.ToU16()
308 goto BetweenKV
309 case jTags[0]:
310 if !utils.FastEqual(jTags, key) {
311 goto invalid
312 }
313 ev.Tags = new(tag.S)
314 if b, err = ev.Tags.Unmarshal(b); chk.E(err) {
315 return
316 }
317 goto BetweenKV
318 case jSig[0]:
319 if !utils.FastEqual(jSig, key) {
320 goto invalid
321 }
322 var sig []byte
323 if sig, b, err = text.UnmarshalHex(b); chk.E(err) {
324 return
325 }
326 if len(sig) != schnorr.SignatureSize {
327 err = errorf.E(
328 "invalid sig length, require %d got %d '%s'\n%s",
329 schnorr.SignatureSize, len(sig), b, b,
330 )
331 return
332 }
333 ev.Sig = sig
334 goto BetweenKV
335 case jContent[0]:
336 if key[1] == jContent[1] {
337 if !utils.FastEqual(jContent, key) {
338 goto invalid
339 }
340 if ev.Content, b, err = text.UnmarshalQuoted(b); chk.T(err) {
341 return
342 }
343 goto BetweenKV
344 } else if key[1] == jCreatedAt[1] {
345 if !utils.FastEqual(jCreatedAt, key) {
346 goto invalid
347 }
348 i := ints.New(0)
349 if b, err = i.Unmarshal(b); chk.T(err) {
350 return
351 }
352 ev.CreatedAt = i.Int64()
353 goto BetweenKV
354 } else {
355 goto invalid
356 }
357 default:
358 goto invalid
359 }
360 BetweenKV:
361 key = key[:0]
362 for ; len(b) > 0; b = b[1:] {
363 // Skip whitespace
364 if isWhitespace(b[0]) {
365 continue
366 }
367 switch {
368 case len(b) == 0:
369 return
370 case b[0] == '}':
371 b = b[1:]
372 goto AfterClose
373 case b[0] == ',':
374 b = b[1:]
375 goto BetweenKeys
376 case b[0] == '"':
377 b = b[1:]
378 goto InKey
379 }
380 }
381 // If we reach here, the buffer ended unexpectedly. Treat as end-of-object
382 goto AfterClose
383 AfterClose:
384 rem = b
385 return
386 invalid:
387 err = fmt.Errorf(
388 "invalid key,\n'%s'\n'%s'\n'%s'", string(b), string(b[:]),
389 string(b),
390 )
391 return
392 eof:
393 err = io.EOF
394 return
395 }
396
397 // UnmarshalJSON unmarshalls a JSON string into an event.E.
398 //
399 // Call ev.Free() to return the provided buffer to the bufpool afterwards.
400 func (ev *E) UnmarshalJSON(b []byte) (err error) {
401 // log.I.F("UnmarshalJSON: '%s'", b)
402 _, err = ev.Unmarshal(b)
403 return
404 }
405
406 // isWhitespace returns true if the byte is a whitespace character (space, tab, newline, carriage return).
407 func isWhitespace(b byte) bool {
408 return b == ' ' || b == '\t' || b == '\n' || b == '\r'
409 }
410
411 // S is an array of event.E that sorts in reverse chronological order.
412 type S []*E
413
414 // Len returns the length of the event.Es.
415 func (ev S) Len() int { return len(ev) }
416
417 // Less returns whether the first is newer than the second (larger unix
418 // timestamp).
419 func (ev S) Less(i, j int) bool { return ev[i].CreatedAt > ev[j].CreatedAt }
420
421 // Swap two indexes of the event.Es with each other.
422 func (ev S) Swap(i, j int) { ev[i], ev[j] = ev[j], ev[i] }
423
424 // C is a channel that carries event.E.
425 type C chan *E
426