nostrdb

an unfairly fast embedded nostr database backed by lmdb
git clone git://jb55.com/nostrdb
Log | Files | Refs | Submodules | README | LICENSE

commit 78cf12171292eda315043ac8e18e0b242a9c5dc4
parent 98a5275bd77898e13c243343555b1c4044e79368
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 25 Jul 2023 19:29:08 -0700

ndb: add websocket event parser

next up is calculating and verifying the hash right off the wire as well

Diffstat:
Mhex.h | 2+-
Mnostrdb.c | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mnostrdb.h | 44+++++++++++++++++++++++++++++++++++++++++---
Mtest.c | 24++++++++++++++++++++++++
4 files changed, 167 insertions(+), 34 deletions(-)

diff --git a/hex.h b/hex.h @@ -40,7 +40,7 @@ static int hex_decode(const char *str, size_t slen, void *buf, size_t bufsize) } -static char hexchar(unsigned int val) +static inline char hexchar(unsigned int val) { if (val < 10) return '0' + val; diff --git a/nostrdb.c b/nostrdb.c @@ -18,6 +18,7 @@ struct ndb_json_parser { struct ndb_builder builder; jsmn_parser json_parser; jsmntok_t *toks, *toks_end; + int i; int num_tokens; }; @@ -102,6 +103,8 @@ static inline int ndb_json_parser_parse(struct ndb_json_parser *p) p->num_tokens = jsmn_parse(&p->json_parser, p->json, p->json_len, p->toks, cap); + p->i = 0; + return p->num_tokens; } @@ -645,92 +648,160 @@ static int parse_unsigned_int(const char *start, int len, unsigned int *num) return 1; } - -int ndb_note_from_json(const char *json, int len, struct ndb_note **note, - unsigned char *buf, int bufsize) +int ndb_ws_event_from_json(const char *json, int len, struct ndb_tce *tce, + unsigned char *buf, int bufsize) { jsmntok_t *tok = NULL; - unsigned char hexbuf[64]; - - int i, tok_len, res; - const char *start; + int tok_len, res; struct ndb_json_parser parser; ndb_json_parser_init(&parser, json, len, buf, bufsize); - res = ndb_json_parser_parse(&parser); - if (res < 0) + if ((res = ndb_json_parser_parse(&parser)) < 0) return res; - if (parser.num_tokens < 1 || parser.toks[0].type != JSMN_OBJECT) + if (parser.num_tokens < 3 || parser.toks[0].type != JSMN_ARRAY) + return 0; + + parser.i = 1; + tok = &parser.toks[parser.i++]; + tok_len = toksize(tok); + if (tok->type != JSMN_STRING) return 0; - for (i = 1; i < parser.num_tokens; i++) { - tok = &parser.toks[i]; + if (tok_len == 5 && !memcmp("EVENT", json + tok->start, 5)) { + tce->evtype = NDB_TCE_EVENT; + struct ndb_event *ev = &tce->event; + + tok = &parser.toks[parser.i++]; + if (tok->type != JSMN_STRING) + return 0; + + ev->subid = json + tok->start; + ev->subid_len = toksize(tok); + + return ndb_parse_json_note(&parser, &ev->note); + } else if (tok_len == 2 && !memcmp("OK", json + tok->start, 2)) { + if (parser.num_tokens != 5) + return 0; + + struct ndb_command_result *cr = &tce->command_result; + + tce->evtype = NDB_TCE_OK; + + tok = &parser.toks[parser.i++]; + if (tok->type != JSMN_STRING) + return 0; + + cr->subid = json + tok->start; + cr->subid_len = toksize(tok); + + tok = &parser.toks[parser.i++]; + if (tok->type != JSMN_PRIMITIVE || toksize(tok) == 0) + return 0; + + cr->ok = (json + tok->start)[0] == 't'; + return 1; + } + + return 0; +} + +int ndb_parse_json_note(struct ndb_json_parser *parser, struct ndb_note **note) +{ + jsmntok_t *tok = NULL; + unsigned char hexbuf[64]; + const char *json = parser->json; + const char *start; + int i, tok_len; + + if (parser->toks[parser->i].type != JSMN_OBJECT) + return 0; + + // TODO: build id buffer and verify at end + + for (i = parser->i + 1; i < parser->num_tokens; i++) { + tok = &parser->toks[i]; start = json + tok->start; tok_len = toksize(tok); //printf("toplevel %.*s %d\n", tok_len, json + tok->start, tok->type); - if (tok_len == 0 || i + 1 >= parser.num_tokens) + if (tok_len == 0 || i + 1 >= parser->num_tokens) continue; if (start[0] == 'p' && jsoneq(json, tok, tok_len, "pubkey")) { // pubkey - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; hex_decode(json + tok->start, toksize(tok), hexbuf, sizeof(hexbuf)); - ndb_builder_set_pubkey(&parser.builder, hexbuf); + ndb_builder_set_pubkey(&parser->builder, hexbuf); } else if (tok_len == 2 && start[0] == 'i' && start[1] == 'd') { // id - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; hex_decode(json + tok->start, toksize(tok), hexbuf, sizeof(hexbuf)); // TODO: validate id - ndb_builder_set_id(&parser.builder, hexbuf); + ndb_builder_set_id(&parser->builder, hexbuf); } else if (tok_len == 3 && start[0] == 's' && start[1] == 'i' && start[2] == 'g') { // sig - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; hex_decode(json + tok->start, toksize(tok), hexbuf, sizeof(hexbuf)); - ndb_builder_set_sig(&parser.builder, hexbuf); + ndb_builder_set_sig(&parser->builder, hexbuf); } else if (start[0] == 'k' && jsoneq(json, tok, tok_len, "kind")) { // kind - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; start = json + tok->start; if (tok->type != JSMN_PRIMITIVE || tok_len <= 0) return 0; if (!parse_unsigned_int(start, toksize(tok), - &parser.builder.note->kind)) + &parser->builder.note->kind)) return 0; } else if (start[0] == 'c') { if (jsoneq(json, tok, tok_len, "created_at")) { // created_at - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; start = json + tok->start; if (tok->type != JSMN_PRIMITIVE || tok_len <= 0) return 0; if (!parse_unsigned_int(start, toksize(tok), - &parser.builder.note->created_at)) + &parser->builder.note->created_at)) return 0; } else if (jsoneq(json, tok, tok_len, "content")) { // content - tok = &parser.toks[i+1]; + tok = &parser->toks[i+1]; union ndb_packed_str pstr; tok_len = toksize(tok); int written, pack_ids = 0; - if (!ndb_builder_make_json_str(&parser.builder, + if (!ndb_builder_make_json_str(&parser->builder, json + tok->start, tok_len, &pstr, &written, pack_ids)) { return 0; } - parser.builder.note->content_length = written; - parser.builder.note->content = pstr; + parser->builder.note->content_length = written; + parser->builder.note->content = pstr; } } else if (start[0] == 't' && jsoneq(json, tok, tok_len, "tags")) { - tok = &parser.toks[i+1]; - ndb_builder_process_json_tags(&parser, tok); + tok = &parser->toks[i+1]; + ndb_builder_process_json_tags(parser, tok); i += tok->size; } } - return ndb_builder_finalize(&parser.builder, note, NULL); + return ndb_builder_finalize(&parser->builder, note, NULL); +} + +int ndb_note_from_json(const char *json, int len, struct ndb_note **note, + unsigned char *buf, int bufsize) +{ + struct ndb_json_parser parser; + int res; + + ndb_json_parser_init(&parser, json, len, buf, bufsize); + if ((res = ndb_json_parser_parse(&parser)) < 0) + return res; + + if (parser.num_tokens < 1) + return 0; + + return ndb_parse_json_note(&parser, note); } void ndb_builder_set_pubkey(struct ndb_builder *builder, unsigned char *pubkey) diff --git a/nostrdb.h b/nostrdb.h @@ -4,6 +4,17 @@ #include <inttypes.h> #include "cursor.h" +// To-client event types +#define NDB_TCE_EVENT 0x1 +#define NDB_TCE_OK 0x2 +#define NDB_TCE_NOTICE 0x3 +#define NDB_TCE_EOSE 0x4 + +#define NDB_PACKED_STR 0x1 +#define NDB_PACKED_ID 0x2 + +struct ndb_json_parser; + struct ndb_str { unsigned char flag; union { @@ -12,9 +23,36 @@ struct ndb_str { }; }; +struct ndb_event { + struct ndb_note *note; + const char *subid; + int subid_len; +}; + +struct ndb_command_result { + int ok; + const char *msg; + int msglen; + const char *subid; + int subid_len; +}; + +// To-client event +struct ndb_tce { + int evtype; + union { + struct ndb_event event; + struct ndb_command_result command_result; + }; +}; + struct ndb_keypair { unsigned char pubkey[32]; unsigned char secret[32]; + + // this corresponds to secp256k1's keypair type. it's guaranteed to + // be 96 bytes according to their docs. I don't want to depend on + // the secp256k1 header here so we just use raw bytes. unsigned char pair[96]; }; @@ -22,9 +60,6 @@ struct ndb_keypair { // representation #pragma pack(push, 1) -/// We can store byte data in the string table, so -#define NDB_PACKED_STR 0x1 -#define NDB_PACKED_ID 0x2 union ndb_packed_str { struct { @@ -91,6 +126,9 @@ int ndb_create_keypair(struct ndb_keypair *key); int ndb_decode_key(const char *secstr, struct ndb_keypair *keypair); // BUILDER + +int ndb_parse_json_note(struct ndb_json_parser *, struct ndb_note **); +int ndb_ws_event_from_json(const char *json, int len, struct ndb_tce *tce, unsigned char *buf, int bufsize); int ndb_note_from_json(const char *json, int len, struct ndb_note **, unsigned char *buf, int buflen); int ndb_builder_init(struct ndb_builder *builder, unsigned char *buf, int bufsize); int ndb_builder_finalize(struct ndb_builder *builder, struct ndb_note **note, struct ndb_keypair *privkey); diff --git a/test.c b/test.c @@ -246,10 +246,34 @@ static void test_strings_work_before_finalization() { assert(!strcmp(ndb_note_str(b->note, &b->note->content).str, "hello")); } +// test to-client event +static void test_tce() { + +#define HEX_ID "5004a081e397c6da9dc2f2d6b3134006a9d0e8c1b46689d9fe150bb2f21a204d" +#define HEX_PK "b169f596968917a1abeb4234d3cf3aa9baee2112e58998d17c6db416ad33fe40" +#define JSON "{\"id\": \"" HEX_ID "\",\"pubkey\": \"" HEX_PK "\",\"created_at\": 1689836342,\"kind\": 1,\"tags\": [[\"p\",\"" HEX_ID "\"], [\"word\", \"words\", \"w\"]],\"content\": \"共通語\",\"sig\": \"e4d528651311d567f461d7be916c37cbf2b4d530e672f29f15f353291ed6df60c665928e67d2f18861c5ca88\"}" + unsigned char buf[1024]; + const char json[] = "[\"EVENT\",\"subid123\"," JSON "]"; + struct ndb_tce tce; + int ok; + + ok = ndb_ws_event_from_json(json, sizeof(json), &tce, buf, sizeof(buf)); + assert(ok); + + assert(tce.evtype == NDB_TCE_EVENT); + assert(tce.event.subid_len == 8); + assert(!memcmp(tce.event.subid, "subid123", 8)); + +#undef HEX_ID +#undef HEX_PK +#undef JSON +} + int main(int argc, const char *argv[]) { test_basic_event(); test_empty_tags(); test_parse_json(); test_parse_contact_list(); test_strings_work_before_finalization(); + test_tce(); }