diff options
| -rw-r--r-- | 93.md | 40 |
1 files changed, 18 insertions, 22 deletions
| @@ -8,9 +8,9 @@ NSON | |||
| 8 | 8 | ||
| 9 | ### Preamble | 9 | ### Preamble |
| 10 | 10 | ||
| 11 | Some [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). | 11 | Some [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 "Sonic" library uses about 50% of that of the standard library). |
| 12 | 12 | ||
| 13 | Meanwhile, 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). | 13 | Meanwhile, doing a simple TLV encoding reduces the decoding time to 35% and a simpler static binary format for Nostr events 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 in [this comment](https://github.com/nostr-protocol/nips/pull/512#issuecomment-1542368664). |
| 14 | 14 | ||
| 15 | ### The actual NIP | 15 | ### The actual NIP |
| 16 | 16 | ||
| @@ -20,31 +20,27 @@ Here's an example of a NSON-encoded Nostr event: | |||
| 20 | 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"]]}` | 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 | 22 | ||
| 23 | The 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). | 23 | The 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 their sizes are given by the values inside the `"nson"` field (which is also dynamic, its size given by its first byte). |
| 24 | 24 | ||
| 25 | ### Anatomy of the `"nson"` field | 25 | ### Anatomy of the `"nson"` field |
| 26 | 26 | ||
| 27 | It is hex-encoded. Some fields are a single byte, others are two bytes (4 characters). | 27 | It is hex-encoded. Some fields are a single byte, others are two bytes (4 characters), big-endian. |
| 28 | 28 | ||
| 29 | Each explanation starts at the same line as the field it is referring to. | 29 | tt: number of tags (let's say it's two) |
| 30 | 30 | nn: number of items on the first tag (let's say it's 3) | |
| 31 | number of tags (let's say it's two) | 31 | 1111: number of chars on the first item |
| 32 | number of items on the first tag (let's say it's three) | 32 | 2222: number of chars on the second item |
| 33 | number of chars on the first item | 33 | 3333: number of chars on the third item |
| 34 | number of chars on the second item | 34 | nn: number of items on the second tag (let's say it's 2) |
| 35 | number of chars on the third item | 35 | 1111: number of chars on the first item |
| 36 | number of items on the second tag (let's say it's two) | 36 | 2222: number of chars on the second item |
| 37 | number of chars on the first item | ||
| 38 | number of chars on the second item | ||
| 39 | "nson":"xxkkccccttnn111122223333nn11112222" | 37 | "nson":"xxkkccccttnn111122223333nn11112222" |
| 40 | nson size | 38 | xx: nson size |
| 41 | kind chars | 39 | kk: kind chars |
| 42 | content chars | 40 | cccc: content chars |
| 43 | 41 | ||
| 44 | ### Reference implementation | 42 | ### Reference implementation |
| 45 | 43 | ||
| 46 | Beware, all Rust maniacs, the following reference implementation is written in Go: | ||
| 47 | |||
| 48 | ```go | 44 | ```go |
| 49 | func decodeNson(data string) *Event { | 45 | func decodeNson(data string) *Event { |
| 50 | evt := &Event{} | 46 | evt := &Event{} |
| @@ -167,13 +163,13 @@ func encodeNson(evt *Event) string { | |||
| 167 | Besides the field ordering and the presence of the `"nson"` field, other restrictions must be applied: | 163 | Besides the field ordering and the presence of the `"nson"` field, other restrictions must be applied: |
| 168 | 164 | ||
| 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. | 165 | - 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. | 166 | - to simplify decoding of `"content"` and `"tags"` strings, escape codes like `\uXXXX` are forbidden in NSON, UTF-8 must be used instead. `\n`, `\\` and `\"` are the only valid escaped sequences. |
| 171 | 167 | ||
| 172 | ### Backwards-compatibility | 168 | ### Backwards-compatibility |
| 173 | 169 | ||
| 174 | Any 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. | 170 | Any reader who is not aware of the NSON-encoding can receive these events and decode them using whatever means they want. The `"nson"` field will just be ignored and life will continue as normal. |
| 175 | 171 | ||
| 176 | Also, 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. | 172 | Also, 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 to anyone. |
| 177 | 173 | ||
| 178 | ### Other points worth mentioning | 174 | ### Other points worth mentioning |
| 179 | 175 | ||