upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfiatjaf <fiatjaf@gmail.com>2023-05-10 21:17:48 -0300
committerfiatjaf <fiatjaf@gmail.com>2023-05-10 21:17:48 -0300
commitbd6d3251960f67bc30d1674115cd847c0799e67e (patch)
tree335b55b6db141fce4a5105d57d883fc06ee43fd0
parent4208652dc7a39c63c39559b13c656ec30400fcba (diff)
initial nson nip draft.
-rw-r--r--93.md184
1 files changed, 184 insertions, 0 deletions
diff --git a/93.md b/93.md
new file mode 100644
index 0000000..121ed5e
--- /dev/null
+++ b/93.md
@@ -0,0 +1,184 @@
1NIP-93
2======
3
4NSON
5----
6
7`draft` `optional` `author:fiatjaf`
8
9### Preamble
10
11Some [benchmarks](https://github.com/fiatjaf/nostr-json-benchmarks/tree/2f254fff91b3ad063ef9726bb4a3d25316cf12d8) made using all libraries available on Golang show that JSON decoding is very slow. And even when people do assembly-level optimizations things only improve up to a point (e.g. for decoding a Nostr event, the decoding time is 50% smaller).
12
13Meanwhile, doing a simple TLV encoding reduces the decoding time to 35% and a simpler static binary format for Nostr events reduces makes that number drop to 4%. However, it would be bad for Nostr if a binary encoding was introduced, as it would be likely to cause compatibility issues, centralize the protocol and/or increase the work for everybody, more about this at [this comment](https://github.com/nostr-protocol/nips/pull/512#issuecomment-1542368664).
14
15### The actual NIP
16
17NSON is a crazy idea that, according to the benchmarks above, reduces the decoding time to 14% of that of the standard library. It works by having the JSON sender encode the event _as JSON_, but in a specific, very strict, order of fields (taking advantage of the fact that Nostr events have static fields, static lengths for some fields, and an overall rigid structure) and include in the JSON object a new field called `"nson"` that contains metadata the JSON receiver can read to help in the decoding process.
18
19Here's an example of a NSON-encoded Nostr event:
20
21`{"id":"57ff66490a6a2af3992accc26ae95f3f60c6e5f84ed0ddf6f59c534d3920d3d2","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"504d142aed7fa7e0f6dab5bcd7eed63963b0277a8e11bbcb03b94531beb4b95a12f1438668b02746bd5362161bc782068e6b71494060975414e793f9e19f57ea","created_at":1683762317,"nson":"2801000b0203000100400005040001004000000014","kind":1,"content":"hello world","tags":[["e","b6de44a9dd47d1c000f795ea0453046914f44ba7d5e369608b04867a575ea83e","reply"],["p","c26f7b252cea77a5b94f42b1a4771021be07d4df766407e47738605f7e3ab774","","wss://relay.damus.io"]]}`
22
23The idea is that `"id"` comes first, so it can be accessed by reading a slice of the string from character `7` to character `71`, `pubkey` from character `83` to `147` and so on. `"content"`, `"kind"` and `"tags"` have dynamic sizes, so these are given by the values inside the `"nson"` field (which is also dynamic, its size by its first byte).
24
25### Anatomy of the `"nson"` field
26
27It is hex-encoded. Some fields are a single byte, others are two bytes (4 characters).
28
29Each explanation starts at the same line as the field it is referring to.
30
31 number of tags (let's say it's two)
32 number of items on the first tag (let's say it's three)
33 number of chars on the first item
34 number of chars on the second item
35 number of chars on the third item
36 number of items on the second tag (let's say it's two)
37 number of chars on the first item
38 number of chars on the second item
39 "nson":"xxkkccccttnn111122223333nn11112222"
40 nson size
41 kind chars
42 content chars
43
44### Reference implementation
45
46Beware, all Rust maniacs, the following reference implementation is written in Go:
47
48```go
49func decodeNson(data string) *Event {
50 evt := &Event{}
51
52 // static fields
53 evt.ID = data[7 : 7+64]
54 evt.PubKey = data[83 : 83+64]
55 evt.Sig = data[156 : 156+128]
56 ts, _ := strconv.ParseInt(data[299:299+10], 10, 64)
57 evt.CreatedAt = Timestamp(ts)
58
59 // nson values
60 nsonSizeBytes, _ := hex.DecodeString(data[318 : 318+2])
61 nsonSize := int(nsonSizeBytes[0])
62 nsonDescriptors, _ := hex.DecodeString(data[320 : 320+nsonSize])
63
64 // dynamic fields
65 // kind
66 kindChars := int(nsonDescriptors[0])
67 kindStart := 320 + nsonSize + 9 // len(`","kind":`)
68 evt.Kind, _ = strconv.Atoi(data[kindStart : kindStart+kindChars])
69
70 // content
71 contentChars := int(binary.BigEndian.Uint16(nsonDescriptors[1:3]))
72 contentStart := kindStart + kindChars + 12 // len(`,"content":"`)
73 evt.Content, _ = strconv.Unquote(`"` + data[contentStart:contentStart+contentChars] + `"`)
74
75 // tags
76 nTags := int(nsonDescriptors[3])
77 evt.Tags = make(Tags, nTags)
78 tagsStart := contentStart + contentChars + 9 // len(`","tags":`)
79
80 nsonIndex := 3
81 tagsIndex := tagsStart
82 for t := 0; t < nTags; t++ {
83 nsonIndex++
84 tagsIndex += 1 // len(`[`) or len(`,`)
85 nItems := int(nsonDescriptors[nsonIndex])
86 tag := make(Tag, nItems)
87 for n := 0; n < nItems; n++ {
88 nsonIndex++
89 itemStart := tagsIndex + 2 // len(`["`) or len(`,"`)
90 itemChars := int(binary.BigEndian.Uint16(nsonDescriptors[nsonIndex:]))
91 nsonIndex++
92 tag[n], _ = strconv.Unquote(`"` + data[itemStart:itemStart+itemChars] + `"`)
93 tagsIndex = itemStart + itemChars + 1 // len(`"`)
94 }
95 tagsIndex += 1 // len(`]`)
96 evt.Tags[t] = tag
97 }
98
99 return evt
100}
101
102func encodeNson(evt *Event) string {
103 // start building the nson descriptors (without the first byte that represents the nson size)
104 nsonBuf := make([]byte, 256)
105
106 // build the tags
107 nTags := len(evt.Tags)
108 nsonBuf[3] = uint8(nTags)
109 nsonIndex := 3 // start here
110
111 tagBuilder := strings.Builder{}
112 tagBuilder.Grow(1000) // a guess
113 tagBuilder.WriteString(`[`)
114 for t, tag := range evt.Tags {
115 nItems := len(tag)
116 nsonIndex++
117 nsonBuf[nsonIndex] = uint8(nItems)
118
119 tagBuilder.WriteString(`[`)
120 for i, item := range tag {
121 v := strconv.Quote(item)
122 nsonIndex++
123 binary.BigEndian.PutUint16(nsonBuf[nsonIndex:], uint16(len(v)-2))
124 nsonIndex++
125 tagBuilder.WriteString(v)
126 if nItems > i+1 {
127 tagBuilder.WriteString(`,`)
128 }
129 }
130 tagBuilder.WriteString(`]`)
131 if nTags > t+1 {
132 tagBuilder.WriteString(`,`)
133 }
134 }
135 tagBuilder.WriteString(`]}`)
136 nsonBuf = nsonBuf[0 : nsonIndex+1]
137
138 kind := strconv.Itoa(evt.Kind)
139 kindChars := len(kind)
140 nsonBuf[0] = uint8(kindChars)
141
142 content := strconv.Quote(evt.Content)
143 contentChars := len(content) - 2
144 binary.BigEndian.PutUint16(nsonBuf[1:3], uint16(contentChars))
145
146 // actually build the json
147 base := strings.Builder{}
148 base.Grow(320 + // everything up to "nson":
149 2 + len(nsonBuf)*2 + // nson
150 9 + kindChars + // kind and its label
151 12 + contentChars + // content and its label
152 9 + tagBuilder.Len() + // tags and its label
153 2, // the end
154 )
155 base.WriteString(`{"id":"` + evt.ID + `","pubkey":"` + evt.PubKey + `","sig":"` + evt.Sig + `","created_at":` + strconv.FormatInt(int64(evt.CreatedAt), 10) + `,"nson":"`)
156 base.WriteString(hex.EncodeToString([]byte{uint8(len(nsonBuf) * 2)})) // nson size
157 base.WriteString(hex.EncodeToString(nsonBuf)) // nson descriptors
158 base.WriteString(`","kind":` + kind + `,"content":` + content + `,"tags":`)
159 base.WriteString(tagBuilder.String() /* includes the end */)
160
161 return base.String()
162}
163```
164
165### Other restrictions
166
167Besides the field ordering and the presence of the `"nson"` field, other restrictions must be applied:
168
169- the `"created_at"` field must have 10, characters, which gives us a range of dates from about 20 years ago up to 250 years in the future.
170- to simplify decoding of `"content"` and `"tags"` strings, escape codes like `\uXXXX` are forbidden in NSON, UTF-8 must be used instead. Only `\n`, `\\` and `\"` are the only valid escaped sequences.
171
172### Backwards-compatibility
173
174Any reader who is not aware of the NSON-encoding can receive these events and decode them using whatever other JSON decoder they happen to have in hand. The `"nson"` field will just be ignored and life will continue as normal.
175
176Also, other event fields that may be present (for example, the NIP-03 `"ots"` field) can be added at the end, after `"tags"`, with no loss.
177
178### Other points worth mentioning
179
180- Relays can receive non-nsonified events from clients, then reformat and store them nsonified so they can serve future clients better by sending them NSON always.
181
182### Open questions to be edited out of the NIP
183
184- How to signal NSON support? I thought it would work to have an initial field `"n":1` (before `"id"`) on the JSON, which could be read very fast, but I don't know.