*Initial version with memory only store

This commit is contained in:
Derrick Hammer 2022-12-30 04:40:14 -05:00
parent b86dd1f1c5
commit d416966760
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
10 changed files with 460 additions and 1 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2022 Hammer Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

19
messages.proto Normal file
View File

@ -0,0 +1,19 @@
syntax = "proto2";
message Query {
required bytes pubkey = 1;
}
enum MessageType {
CREATE = 1;
CREATED = 2;
RESPONSE = 3;
}
message Message {
required MessageType type = 1;
required bytes pubkey = 2;
required uint32 revision = 3;
required bytes data = 4;
required bytes signature = 5;
}

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "@lumeweb/relay-plugin-registry",
"type": "module",
"version": "0.1.0",
"scripts": {
"build": "rollup -c rollup.config.js"
},
"dependencies": {
"@lumeweb/dht-flood": "https://git.lumeweb.com/LumeWeb/dht-flood.git",
"@lumeweb/relay-plugin-rollup-preset": "https://git.lumeweb.com/LumeWeb/relay-plugin-rollup-preset.git",
"@lumeweb/relay-types": "https://git.lumeweb.com/LumeWeb/relay-types.git",
"@noble/ed25519": "^1.7.1",
"b4a": "^1.6.1",
"node-cache": "^5.1.2",
"object-merger": "^1.0.3"
},
"devDependencies": {
"@lumeweb/cfg": "https://git.lumeweb.com/LumeWeb/cfg.git",
"@protobuf-ts/plugin": "^2.8.2",
"@protobuf-ts/runtime": "^2.8.2",
"@types/node": "^18.11.18",
"prettier": "^2.8.1"
}
}

5
pkg/load-registry.json Normal file
View File

@ -0,0 +1,5 @@
{
"plugins": [
"registry"
]
}

13
rollup.config.js Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from "rollup";
import preset from "@lumeweb/relay-plugin-rollup-preset";
import merger from "object-merger";
export default defineConfig(
merger(preset(), {
input: "src/index.ts",
output: {
file: "dist/registry.js",
format: "cjs",
},
})
);

150
src/index.ts Normal file
View File

@ -0,0 +1,150 @@
import type { Plugin, PluginAPI } from "@lumeweb/relay-types";
import DHTFlood from "@lumeweb/dht-flood";
import { Message, Query, MessageType } from "./messages.js";
import EventEmitter from "events";
import NodeCache from "node-cache";
import b4a from "b4a";
import { SignedRegistryEntry } from "./types.js";
import { verifyEntry } from "./utils.js";
const PROTOCOL = "lumeweb.registry";
const events = new EventEmitter();
let messenger: DHTFlood;
let api: PluginAPI;
let memStore: NodeCache;
function setup() {
messenger = new DHTFlood({
swarm: api.swarm,
protocol: PROTOCOL,
id: api.identity.publicKeyRaw as Buffer,
});
messenger.on("message", (data: Buffer, origin: Buffer) => {
try {
let response = Message.fromBinary(data);
switch (response.type) {
case MessageType.CREATE:
events.emit("create", response, origin);
break;
case MessageType.CREATED:
events.emit("created", response, origin);
break;
case MessageType.RESPONSE:
events.emit("response", response, origin);
break;
}
return;
} catch {}
try {
let query = Query.fromBinary(data);
events.emit("response", query, origin);
} catch {}
});
}
function entryFromMessage(message: Message): SignedRegistryEntry {
return {
pk: message.pubkey,
data: message.data,
revision: message.revision,
signature: message.signature,
};
}
function sendDirectOrBroadcast(message: Message, pubkey: Buffer) {
let peer = api.swarm._allConnections.get(pubkey);
let data = Message.toBinary(message);
if (peer) {
messenger.send(peer, data, 0);
return;
}
messenger.broadcast(data);
}
async function getEntry(
pubkey: Uint8Array
): Promise<SignedRegistryEntry | boolean> {
let pubkeyHex = b4a.from(pubkey).toString("hex");
if (memStore) {
return await memStore.get<SignedRegistryEntry>(pubkeyHex);
}
return false;
}
async function setEntry(entry: SignedRegistryEntry): Promise<boolean> {
let pubkeyHex = b4a.from(entry.pk).toString("hex");
if (memStore) {
return await memStore.set<SignedRegistryEntry>(pubkeyHex, entry);
}
return false;
}
const plugin: Plugin = {
name: "registry",
async plugin(_api: PluginAPI): Promise<void> {
api = _api;
setup();
if (
api.pluginConfig.bool("storememory") &&
!api.pluginConfig.bool("store")
) {
memStore = new NodeCache();
}
events.on("create", async (message: Message, origin: Buffer) => {
let newEntry = entryFromMessage(message);
if (!verifyEntry(newEntry)) {
return;
}
let entry = (await getEntry(newEntry.pk)) as SignedRegistryEntry;
async function setAndRespond() {
await setEntry(newEntry);
sendDirectOrBroadcast(
Message.create({
type: MessageType.CREATED,
pubkey: newEntry.pk,
revision: newEntry.revision,
signature: newEntry.signature,
data: newEntry.data,
}),
origin
);
}
if (entry) {
if (newEntry.revision <= entry.revision) {
setAndRespond();
}
} else {
setAndRespond();
}
});
events.on("query", async (query: Query, origin: Buffer) => {
let entry = (await getEntry(query.pubkey)) as SignedRegistryEntry;
if (entry) {
sendDirectOrBroadcast(
Message.create({
type: MessageType.RESPONSE,
pubkey: entry.pk,
revision: entry.revision,
signature: entry.signature,
data: entry.data,
}),
origin
);
}
});
},
};
export default plugin;

190
src/messages.ts Normal file
View File

@ -0,0 +1,190 @@
// @generated by protobuf-ts 2.8.2
// @generated from protobuf file "messages.proto" (syntax proto2)
// tslint:disable
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType as MessageType$ } from "@protobuf-ts/runtime";
/**
* @generated from protobuf message Query
*/
export interface Query {
/**
* @generated from protobuf field: bytes pubkey = 1;
*/
pubkey: Uint8Array;
}
/**
* @generated from protobuf message Message
*/
export interface Message {
/**
* @generated from protobuf field: MessageType type = 1;
*/
type: MessageType;
/**
* @generated from protobuf field: bytes pubkey = 2;
*/
pubkey: Uint8Array;
/**
* @generated from protobuf field: uint32 revision = 3;
*/
revision: number;
/**
* @generated from protobuf field: bytes data = 4;
*/
data: Uint8Array;
/**
* @generated from protobuf field: bytes signature = 5;
*/
signature: Uint8Array;
}
/**
* @generated from protobuf enum MessageType
*/
export enum MessageType {
/**
* @generated synthetic value - protobuf-ts requires all enums to have a 0 value
*/
UNSPECIFIED$ = 0,
/**
* @generated from protobuf enum value: CREATE = 1;
*/
CREATE = 1,
/**
* @generated from protobuf enum value: CREATED = 2;
*/
CREATED = 2,
/**
* @generated from protobuf enum value: RESPONSE = 3;
*/
RESPONSE = 3
}
// @generated message type with reflection information, may provide speed optimized methods
class Query$Type extends MessageType$<Query> {
constructor() {
super("Query", [
{ no: 1, name: "pubkey", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<Query>): Query {
const message = { pubkey: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<Query>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Query): Query {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bytes pubkey */ 1:
message.pubkey = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Query, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes pubkey = 1; */
if (message.pubkey.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.pubkey);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Query
*/
export const Query = new Query$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Message$Type extends MessageType$<Message> {
constructor() {
super("Message", [
{ no: 1, name: "type", kind: "enum", T: () => ["MessageType", MessageType] },
{ no: 2, name: "pubkey", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 3, name: "revision", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
{ no: 4, name: "data", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
{ no: 5, name: "signature", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<Message>): Message {
const message = { type: 0, pubkey: new Uint8Array(0), revision: 0, data: new Uint8Array(0), signature: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<Message>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Message): Message {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* MessageType type */ 1:
message.type = reader.int32();
break;
case /* bytes pubkey */ 2:
message.pubkey = reader.bytes();
break;
case /* uint32 revision */ 3:
message.revision = reader.uint32();
break;
case /* bytes data */ 4:
message.data = reader.bytes();
break;
case /* bytes signature */ 5:
message.signature = reader.bytes();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* MessageType type = 1; */
if (message.type !== 0)
writer.tag(1, WireType.Varint).int32(message.type);
/* bytes pubkey = 2; */
if (message.pubkey.length)
writer.tag(2, WireType.LengthDelimited).bytes(message.pubkey);
/* uint32 revision = 3; */
if (message.revision !== 0)
writer.tag(3, WireType.Varint).uint32(message.revision);
/* bytes data = 4; */
if (message.data.length)
writer.tag(4, WireType.LengthDelimited).bytes(message.data);
/* bytes signature = 5; */
if (message.signature.length)
writer.tag(5, WireType.LengthDelimited).bytes(message.signature);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Message
*/
export const Message = new Message$Type();

12
src/types.ts Normal file
View File

@ -0,0 +1,12 @@
export interface SignedRegistryEntry {
pk: Uint8Array;
// revision number of this entry, maximum is (256^8)-1
revision: number;
// data stored in this entry, can have a maximum length of 48 bytes
data: Uint8Array;
// signature of this registry entry
signature?: Uint8Array;
}

33
src/utils.ts Normal file
View File

@ -0,0 +1,33 @@
import { SignedRegistryEntry } from "./types.js";
import * as ed from "@noble/ed25519";
import b4a from "b4a";
export function verifyEntry(entry: SignedRegistryEntry) {
return ed.sync.verify(entry.signature, createSignatureData(entry), entry.pk);
}
export function signEntry(
entry: SignedRegistryEntry,
privateKey: Uint8Array
): Uint8Array {
return ed.sync.sign(createSignatureData(entry), privateKey);
}
export function createSignatureData(entry: SignedRegistryEntry): Uint8Array {
return b4a.concat([
encodeEndian(entry.revision, 8),
entry.data.length,
entry.data,
]);
}
export function encodeEndian(value: number, length: number) {
let res = new Uint8Array(length);
for (let i = 0; i < length; i++) {
res[i] = value & 0xff;
value = value >> 8;
}
return res;
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"esModuleInterop": true,
"outDir": "dist",
"moduleResolution": "node"
},
"include": ["src"],
"exclude": [
"node_modules"
]
}