Browse Source

Key mishmatch warning, new serialization handler

pull/780/head
philon- 11 months ago
parent
commit
1c5bafe8de
  1. 63
      packages/web/src/core/stores/nodeDBStore/index.ts
  2. 8
      packages/web/src/core/stores/nodeDBStore/types.ts
  3. 95
      packages/web/src/core/stores/utils/indexDB.ts
  4. 27
      packages/web/src/core/subscriptions.ts

63
packages/web/src/core/stores/nodeDBStore/index.ts

@ -4,7 +4,7 @@ import { Protobuf, type Types } from "@meshtastic/core";
import { produce } from "immer"; import { produce } from "immer";
import { create as createStore } from "zustand"; import { create as createStore } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import type { NodeError, ProcessPacketParams } from "./types"; import type { NodeError, NodeErrorType, ProcessPacketParams } from "./types";
const CURRENT_STORE_VERSION = 0; const CURRENT_STORE_VERSION = 0;
@ -22,7 +22,7 @@ export interface NodeDB {
updateFavorite: (nodeNum: number, isFavorite: boolean) => void; updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
updateIgnore: (nodeNum: number, isIgnored: boolean) => void; updateIgnore: (nodeNum: number, isIgnored: boolean) => void;
setNodeNum: (nodeNum: number) => void; setNodeNum: (nodeNum: number) => void;
setNodeError: (nodeNum: number, error: string) => void; setNodeError: (nodeNum: number, error: NodeErrorType) => void;
clearNodeError: (nodeNum: number) => void; clearNodeError: (nodeNum: number) => void;
getNodesLength: () => number; getNodesLength: () => number;
@ -58,17 +58,16 @@ type NodeDBPersisted = {
nodeDBs: Map<number, NodeDBData>; nodeDBs: Map<number, NodeDBData>;
}; };
function mergeIntoCurrent(target: NodeDB, incoming: NodeDB) { export function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
for (const [n, info] of incoming.nodeMap) { if (a.byteLength !== b.byteLength) {
if (!target.nodeMap.has(n)) { return false;
target.nodeMap.set(n, info);
} }
} for (let i = 0; i < a.byteLength; i++) {
for (const [n, err] of incoming.nodeErrors) { if (a[i] !== b[i]) {
if (!target.nodeErrors.has(n)) { return false;
target.nodeErrors.set(n, err);
} }
} }
return true;
} }
function nodeDBFactory( function nodeDBFactory(
@ -79,7 +78,7 @@ function nodeDBFactory(
): NodeDB { ): NodeDB {
const nodeMap = data?.nodeMap ?? new Map<number, Protobuf.Mesh.NodeInfo>(); const nodeMap = data?.nodeMap ?? new Map<number, Protobuf.Mesh.NodeInfo>();
const nodeErrors = data?.nodeErrors ?? new Map<number, NodeError>(); const nodeErrors = data?.nodeErrors ?? new Map<number, NodeError>();
let myNodeNum = data?.myNodeNum; const myNodeNum = data?.myNodeNum;
return { return {
id, id,
@ -205,26 +204,30 @@ function nodeDBFactory(
setNodeNum: (nodeNum) => setNodeNum: (nodeNum) =>
set( set(
produce<PrivateNodeDBState>((draft) => { produce<PrivateNodeDBState>((draft) => {
console.debug( const newDB = draft.nodeDBs.get(id);
`Setting myNodeNum for nodeDB with id: ${id} to ${nodeNum}`, if (!newDB) {
);
const current = draft.nodeDBs.get(id);
if (!current) {
throw new Error(`No nodeDB found for id: ${id}`); throw new Error(`No nodeDB found for id: ${id}`);
} }
// There might already be an entry for this nodeNum, so we need to merge newDB.myNodeNum = nodeNum;
// it into the current nodeDB.
myNodeNum = nodeNum;
current.myNodeNum = nodeNum;
for (const [k, other] of draft.nodeDBs) { for (const [key, oldDB] of draft.nodeDBs) {
if (k === id) { if (key === id) {
continue; continue;
} }
if (other.myNodeNum === nodeNum) { if (oldDB.myNodeNum === nodeNum) {
mergeIntoCurrent(current, other); // current wins on conflicts // The new DB is typically empty when nodenum is set, so we can safely copy over from the old DB
draft.nodeDBs.delete(k); // otherwise, discard the old DB completely
if (newDB.nodeMap.size === 0) {
newDB.nodeMap = oldDB.nodeMap;
newDB.nodeErrors = oldDB.nodeErrors;
} else {
console.error(
`NodeDB with id: ${id} already has nodes, not merging with old DB`,
);
}
draft.nodeDBs.delete(key);
} }
} }
}), }),
@ -384,7 +387,12 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
if (!state) { if (!state) {
return; return;
} }
console.debug("NodeDBStore: Rehydrating state", state); console.debug(
"NodeDBStore: Rehydrating state with ",
state.nodeDBs.size,
" nodeDBs -",
state.nodeDBs,
);
useNodeDBStore.setState( useNodeDBStore.setState(
produce<PrivateNodeDBState>((draft) => { produce<PrivateNodeDBState>((draft) => {
@ -392,6 +400,8 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
for (const [id, data] of ( for (const [id, data] of (
draft.nodeDBs as unknown as Map<number, NodeDBData> draft.nodeDBs as unknown as Map<number, NodeDBData>
).entries()) { ).entries()) {
if (data.myNodeNum !== undefined) {
// Only rebuild if there is a nodenum set otherwise orphan dbs will acumulate
rebuilt.set( rebuilt.set(
id, id,
nodeDBFactory( nodeDBFactory(
@ -402,6 +412,7 @@ export const useNodeDBStore = createStore<PrivateNodeDBState>()(
), ),
); );
} }
}
draft.nodeDBs = rebuilt; draft.nodeDBs = rebuilt;
}), }),
); );

8
packages/web/src/core/stores/nodeDBStore/types.ts

@ -1,6 +1,10 @@
import type { Protobuf } from "@meshtastic/core";
type NodeErrorType = Protobuf.Mesh.Routing_Error | "MISMATCH_PKI";
type NodeError = { type NodeError = {
node: number; node: number;
error: string; error: NodeErrorType;
}; };
type ProcessPacketParams = { type ProcessPacketParams = {
@ -9,4 +13,4 @@ type ProcessPacketParams = {
time: number; time: number;
}; };
export type { NodeError, ProcessPacketParams }; export type { NodeError, ProcessPacketParams, NodeErrorType };

95
packages/web/src/core/stores/utils/indexDB.ts

@ -17,40 +17,91 @@ export const zustandIndexDBStorage: StateStorage = {
}, },
}; };
type SerializedMap<K = unknown, V = unknown> = { type AnyRecord = Record<string, unknown>;
__dataType: "Map";
value: Array<[K, V]>; type Envelope<Tag extends string = string, V = unknown> = {
__datatype: Tag;
value: V;
};
interface Handler<T, Tag extends string, Encoded> {
tag: Tag;
test(val: unknown): val is T;
serialize(val: T): Encoded;
revive(encoded: Encoded): T;
}
function isObject(x: unknown): x is AnyRecord {
return typeof x === "object" && x !== null;
}
function isEnvelope(x: unknown): x is Envelope {
return isObject(x) && typeof x.__datatype === "string" && "value" in x;
}
// Map handler
type SerializedMap<K = unknown, V = unknown> = Array<[K, V]>;
const mapHandler: Handler<Map<unknown, unknown>, "Map", SerializedMap> = {
tag: "Map",
test: (val): val is Map<unknown, unknown> => val instanceof Map,
serialize: (map) => Array.from(map.entries()),
revive: (pairs) => new Map(pairs),
};
// Uint8Array handler
const uint8ArrayHandler: Handler<Uint8Array, "Uint8Array", number[]> = {
tag: "Uint8Array",
test: (val): val is Uint8Array => val instanceof Uint8Array,
serialize: (uint8array) => Array.from(uint8array),
revive: (arr) => new Uint8Array(arr),
}; };
type JsonReplacer = (key: string, value: unknown) => unknown; const defaultHandlers = [mapHandler, uint8ArrayHandler] as const;
const replacer: JsonReplacer = (_, value) => {
if (value instanceof Map) { function makeJson<H extends Handler<unknown, string, unknown>>(
const map = value as Map<unknown, unknown>; handlers: readonly H[],
const serialized: SerializedMap = { ) {
__dataType: "Map", const byTag = new Map<H["tag"], H>();
value: Array.from(map.entries()), for (const handler of handlers) {
byTag.set(handler.tag, handler);
}
const replacer = (_: string, value: unknown): unknown => {
for (const handler of handlers) {
if (handler.test(value)) {
const encoded = handler.serialize(value as never);
const envelope: Envelope<typeof handler.tag, typeof encoded> = {
__datatype: handler.tag,
value: encoded,
}; };
return serialized; return envelope;
}
} }
return value; return value;
}; };
type JsonReviver = (key: string, value: unknown) => unknown; const reviver = (_: string, value: unknown): unknown => {
function isSerializedMap(value: unknown): value is SerializedMap { if (isEnvelope(value)) {
if (typeof value !== "object" || value === null || Array.isArray(value)) { const handler = byTag.get(value.__datatype);
return false; if (handler) {
} return handler.revive(
const potentialMap = value as Partial<SerializedMap>; (value as Envelope<H["tag"], unknown>).value as never,
return potentialMap.__dataType === "Map" && Array.isArray(potentialMap.value); );
} }
const reviver: JsonReviver = (_, value) => {
if (isSerializedMap(value)) {
return new Map(value.value);
} }
return value; return value;
}; };
export function createStorageWithMapSupport<T>(): PersistStorage<T> { return { replacer, reviver };
}
export function createStorageWithMapSupport<
T,
H extends Handler<unknown, string, unknown> = never,
>(extraHandlers: readonly H[] = [] as const): PersistStorage<T> {
const { replacer, reviver } = makeJson([
...defaultHandlers,
...extraHandlers,
]);
return { return {
getItem: async (name): Promise<StorageValue<T> | null> => { getItem: async (name): Promise<StorageValue<T> | null> => {
const str = await zustandIndexDBStorage.getItem(name); const str = await zustandIndexDBStorage.getItem(name);

27
packages/web/src/core/subscriptions.ts

@ -7,6 +7,7 @@ import {
type NodeDB, type NodeDB,
} from "@core/stores"; } from "@core/stores";
import { type MeshDevice, Protobuf } from "@meshtastic/core"; import { type MeshDevice, Protobuf } from "@meshtastic/core";
import { fromByteArray } from "base64-js";
export const subscribeAll = ( export const subscribeAll = (
device: Device, device: Device,
@ -72,6 +73,22 @@ export const subscribeAll = (
connection.events.onNodeInfoPacket.subscribe((nodeInfo) => { connection.events.onNodeInfoPacket.subscribe((nodeInfo) => {
const nodeWithUser = ensureDefaultUser(nodeInfo); const nodeWithUser = ensureDefaultUser(nodeInfo);
if (
nodeWithUser.num !== myNodeNum && // Ignore my own node
nodeDB.getNode(nodeWithUser.num)?.user?.publicKey !== undefined && // Only flag if the old key is not undefined
fromByteArray(
nodeDB.getNode(nodeWithUser.num)?.user?.publicKey ?? new Uint8Array(), // ... and equal to the new key
) !== fromByteArray(nodeWithUser.user?.publicKey ?? new Uint8Array())
) {
console.warn(
`Node ${nodeWithUser.num} has a different public key than expected`,
);
nodeDB.setNodeError(nodeWithUser.num, "MISMATCH_PKI");
// TODO: Handle this error case properly (refactor PKI dialog?)
}
nodeDB.addNode(nodeWithUser); nodeDB.addNode(nodeWithUser);
}); });
@ -131,21 +148,21 @@ export const subscribeAll = (
if (routingPacket.data.variant.case === "errorReason") { if (routingPacket.data.variant.case === "errorReason") {
switch (routingPacket.data.variant.value) { switch (routingPacket.data.variant.value) {
case Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT: case Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT:
console.error(`Routing Error: ${routingPacket.data.variant.value}`); console.error(`Routing Error: $routingPacket.data.variant.value`);
break; break;
case Protobuf.Mesh.Routing_Error.NO_CHANNEL: case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
console.error(`Routing Error: ${routingPacket.data.variant.value}`); console.error(`Routing Error: $routingPacket.data.variant.value`);
nodeDB.setNodeError( nodeDB.setNodeError(
routingPacket.from, routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], routingPacket?.data?.variant?.value,
); );
device.setDialogOpen("refreshKeys", true); device.setDialogOpen("refreshKeys", true);
break; break;
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY: case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
console.error(`Routing Error: ${routingPacket.data.variant.value}`); console.error(`Routing Error: $routingPacket.data.variant.value`);
nodeDB.setNodeError( nodeDB.setNodeError(
routingPacket.from, routingPacket.from,
Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value], routingPacket?.data?.variant?.value,
); );
device.setDialogOpen("refreshKeys", true); device.setDialogOpen("refreshKeys", true);
break; break;

Loading…
Cancel
Save