Add option to use nostr-wasm to verify events

This commit is contained in:
hzrd149
2024-04-15 12:16:59 -05:00
parent 8a24016fa7
commit 958a8506f4
15 changed files with 142 additions and 31 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to use nostr-wasm to verify events

View File

@@ -66,7 +66,8 @@
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"ngeohash": "^0.6.3", "ngeohash": "^0.6.3",
"nostr-idb": "^2.1.1", "nostr-idb": "^2.1.1",
"nostr-tools": "^2.4.0", "nostr-tools": "^2.5.0",
"nostr-wasm": "^0.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -1,8 +1,9 @@
import { AbstractRelay, verifyEvent } from "nostr-tools"; import { AbstractRelay } from "nostr-tools";
import { logger } from "../helpers/debug"; import { logger } from "../helpers/debug";
import { validateRelayURL } from "../helpers/relay"; import { validateRelayURL } from "../helpers/relay";
import { offlineMode } from "../services/offline-mode"; import { offlineMode } from "../services/offline-mode";
import Subject from "./subject"; import Subject from "./subject";
import verifyEventMethod from "../services/verify-event";
export default class RelayPool { export default class RelayPool {
relays = new Map<string, AbstractRelay>(); relays = new Map<string, AbstractRelay>();
@@ -28,7 +29,7 @@ export default class RelayPool {
url = validateRelayURL(url); url = validateRelayURL(url);
const key = url.toString(); const key = url.toString();
if (!this.relays.has(key)) { if (!this.relays.has(key)) {
const newRelay = new AbstractRelay(key, { verifyEvent }); const newRelay = new AbstractRelay(key, { verifyEvent: verifyEventMethod });
this.relays.set(key, newRelay); this.relays.set(key, newRelay);
this.onRelayCreated.next(newRelay); this.onRelayCreated.next(newRelay);
} }

View File

@@ -4,10 +4,7 @@ import { logger } from "../helpers/debug";
import { safeRelayUrl } from "../helpers/relay"; import { safeRelayUrl } from "../helpers/relay";
import WasmRelay from "./wasm-relay"; import WasmRelay from "./wasm-relay";
import MemoryRelay from "../classes/memory-relay"; import MemoryRelay from "../classes/memory-relay";
import { fakeVerifyEvent } from "./verify-event";
function fakeVerify(event: NostrEvent): event is VerifiedEvent {
return (event[verifiedSymbol] = true);
}
// save the local relay from query params to localStorage // save the local relay from query params to localStorage
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
@@ -22,7 +19,10 @@ if (paramRelay) {
export const NOSTR_RELAY_TRAY_URL = "ws://localhost:4869/"; export const NOSTR_RELAY_TRAY_URL = "ws://localhost:4869/";
export async function checkNostrRelayTray() { export async function checkNostrRelayTray() {
return new Promise((res) => { return new Promise((res) => {
const test = new AbstractRelay(NOSTR_RELAY_TRAY_URL, { verifyEvent: fakeVerify }); const test = new AbstractRelay(NOSTR_RELAY_TRAY_URL, {
// presume events from the cache are already verified
verifyEvent: fakeVerifyEvent,
});
test test
.connect() .connect()
.then(() => { .then(() => {
@@ -54,14 +54,14 @@ async function createRelay() {
} else if (localRelayURL.startsWith("nostr-idb://")) { } else if (localRelayURL.startsWith("nostr-idb://")) {
return createInternalRelay(); return createInternalRelay();
} else if (safeRelayUrl(localRelayURL)) { } else if (safeRelayUrl(localRelayURL)) {
return new AbstractRelay(safeRelayUrl(localRelayURL)!, { verifyEvent: fakeVerify }); return new AbstractRelay(safeRelayUrl(localRelayURL)!, { verifyEvent: fakeVerifyEvent });
} }
} else if (window.satellite) { } else if (window.satellite) {
return new AbstractRelay(await window.satellite.getLocalRelay(), { verifyEvent: fakeVerify }); return new AbstractRelay(await window.satellite.getLocalRelay(), { verifyEvent: fakeVerifyEvent });
} else if (window.CACHE_RELAY_ENABLED) { } else if (window.CACHE_RELAY_ENABLED) {
const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const protocol = location.protocol === "https:" ? "wss:" : "ws:";
return new AbstractRelay(new URL(protocol + location.host + "/local-relay").toString(), { return new AbstractRelay(new URL(protocol + location.host + "/local-relay").toString(), {
verifyEvent: fakeVerify, verifyEvent: fakeVerifyEvent,
}); });
} }
return createInternalRelay(); return createInternalRelay();

View File

@@ -11,6 +11,7 @@ import createDefer, { Deferred } from "../classes/deferred";
import { truncatedId } from "../helpers/nostr/event"; import { truncatedId } from "../helpers/nostr/event";
import { NostrConnectAccount } from "./account"; import { NostrConnectAccount } from "./account";
import { safeRelayUrl } from "../helpers/relay"; import { safeRelayUrl } from "../helpers/relay";
import { alwaysVerify } from "./verify-event";
export function isErrorResponse(response: any): response is NostrConnectErrorResponse { export function isErrorResponse(response: any): response is NostrConnectErrorResponse {
return !!response.error; return !!response.error;
@@ -112,6 +113,7 @@ export class NostrConnectClient {
private requests = new Map<string, Deferred<any>>(); private requests = new Map<string, Deferred<any>>();
private auths = new Set<string>(); private auths = new Set<string>();
async handleEvent(event: NostrEvent) { async handleEvent(event: NostrEvent) {
if (!alwaysVerify(event)) return;
if (this.provider && event.pubkey !== this.provider) return; if (this.provider && event.pubkey !== this.provider) return;
const to = event.tags.find(isPTag)?.[1]; const to = event.tags.find(isPTag)?.[1];

View File

@@ -8,6 +8,7 @@ import { localRelay } from "./local-relay";
import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats"; import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats";
import relayPoolService from "./relay-pool"; import relayPoolService from "./relay-pool";
import { Filter } from "nostr-tools"; import { Filter } from "nostr-tools";
import { alwaysVerify } from "./verify-event";
const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f"; const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f";
const MONITOR_RELAY = "wss://history.nostr.watch"; const MONITOR_RELAY = "wss://history.nostr.watch";
@@ -24,6 +25,8 @@ class RelayStatsService {
} }
handleEvent(event: NostrEvent, cache = true) { handleEvent(event: NostrEvent, cache = true) {
if (!alwaysVerify(event)) return;
// ignore all events before NIP-66 start date // ignore all events before NIP-66 start date
if (event.created_at < 1704196800) return; if (event.created_at < 1704196800) return;

View File

@@ -12,6 +12,7 @@ import EventStore from "../classes/event-store";
import Subject from "../classes/subject"; import Subject from "../classes/subject";
import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader"; import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader";
import relayPoolService from "./relay-pool"; import relayPoolService from "./relay-pool";
import { alwaysVerify } from "./verify-event";
export type RequestOptions = { export type RequestOptions = {
/** Always request the event from the relays */ /** Always request the event from the relays */
@@ -43,6 +44,7 @@ class ReplaceableEventsService {
dbLog = this.log.extend("database"); dbLog = this.log.extend("database");
handleEvent(event: NostrEvent, saveToCache = true) { handleEvent(event: NostrEvent, saveToCache = true) {
if (!alwaysVerify(event)) return;
const cord = getEventCoordinate(event); const cord = getEventCoordinate(event);
const subject = this.subjects.get(cord); const subject = this.subjects.get(cord);

View File

@@ -10,6 +10,7 @@ export type AppSettingsV0 = {
autoShowMedia: boolean; autoShowMedia: boolean;
proxyUserMedia: boolean; proxyUserMedia: boolean;
showReactions: boolean; showReactions: boolean;
/** @deprecated */
showSignatureVerification: boolean; showSignatureVerification: boolean;
autoPayWithWebLN: boolean; autoPayWithWebLN: boolean;
@@ -53,6 +54,7 @@ export const defaultSettings: AppSettings = {
proxyUserMedia: false, proxyUserMedia: false,
loadOpenGraphData: true, loadOpenGraphData: true,
showReactions: true, showReactions: true,
/** @deprecated */
showSignatureVerification: false, showSignatureVerification: false,
noteDifficulty: null, noteDifficulty: null,

View File

@@ -0,0 +1,44 @@
import { NostrEvent, VerifiedEvent, verifiedSymbol, verifyEvent } from "nostr-tools";
import { logger } from "../../helpers/debug";
const localStorageKey = "verify-event-method";
const log = logger.extend("VerifyEvent");
let selectedMethod = "default";
let verifyEventMethod: typeof verifyEvent;
let alwaysVerify: typeof verifyEvent;
export function fakeVerifyEvent(event: NostrEvent): event is VerifiedEvent {
return (event[verifiedSymbol] = true);
}
try {
selectedMethod = localStorage.getItem(localStorageKey) ?? "default";
switch (selectedMethod) {
case "wasm":
if (!("WebAssembly" in window)) throw new Error("WebAssembly not supported");
log("Loading WebAssembly module");
verifyEventMethod = alwaysVerify = (await import("./wasm")).default;
log("Loaded");
break;
case "none":
log("Using fake verify event method");
verifyEventMethod = fakeVerifyEvent;
alwaysVerify = verifyEvent;
break;
case "default":
default:
log("Using nostr-tools default");
verifyEventMethod = alwaysVerify = verifyEvent;
break;
}
} catch (error) {
console.error("Failed to initialize event verification method, falling back to default");
console.log(error);
verifyEventMethod = alwaysVerify = verifyEvent;
}
export { alwaysVerify, selectedMethod };
export default verifyEventMethod;

View File

@@ -0,0 +1,7 @@
import { setNostrWasm, verifyEvent } from "nostr-tools/wasm";
import { initNostrWasm } from "nostr-wasm";
const wasm = await initNostrWasm();
setNostrWasm(wasm);
export default verifyEvent;

3
src/types/nostr-tools-wasm.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module "nostr-tools/wasm" {
export * from "nostr-tools/lib/types/wasm.d.ts";
}

View File

@@ -40,7 +40,7 @@ export default function DisplaySettings() {
<FormLabel htmlFor="theme" mb="0"> <FormLabel htmlFor="theme" mb="0">
Theme Theme
</FormLabel> </FormLabel>
<Select id="theme" {...register("theme")}> <Select id="theme" {...register("theme")} maxW="sm">
<option value="default">Default</option> <option value="default">Default</option>
<option value="chakraui">ChakraUI</option> <option value="chakraui">ChakraUI</option>
</Select> </Select>
@@ -49,7 +49,7 @@ export default function DisplaySettings() {
<FormLabel htmlFor="colorMode" mb="0"> <FormLabel htmlFor="colorMode" mb="0">
Color Mode Color Mode
</FormLabel> </FormLabel>
<Select id="colorMode" {...register("colorMode")}> <Select id="colorMode" {...register("colorMode")} maxW="sm">
<option value="system">System Default</option> <option value="system">System Default</option>
<option value="light">Light</option> <option value="light">Light</option>
<option value="dark">Dark</option> <option value="dark">Dark</option>
@@ -67,7 +67,7 @@ export default function DisplaySettings() {
<FormLabel htmlFor="maxPageWidth" mb="0"> <FormLabel htmlFor="maxPageWidth" mb="0">
Max Page width Max Page width
</FormLabel> </FormLabel>
<Select id="maxPageWidth" {...register("maxPageWidth")}> <Select id="maxPageWidth" {...register("maxPageWidth")} maxW="sm">
<option value="none">None</option> <option value="none">None</option>
<option value="md">Medium (~768px)</option> <option value="md">Medium (~768px)</option>
<option value="lg">Large (~992px)</option> <option value="lg">Large (~992px)</option>
@@ -123,7 +123,12 @@ export default function DisplaySettings() {
<FormLabel htmlFor="muted-words" mb="0"> <FormLabel htmlFor="muted-words" mb="0">
Muted words Muted words
</FormLabel> </FormLabel>
<Textarea id="muted-words" {...register("mutedWords")} placeholder="Broccoli, Spinach, Artichoke..." /> <Textarea
id="muted-words"
{...register("mutedWords")}
placeholder="Broccoli, Spinach, Artichoke..."
maxW="2xl"
/>
<FormHelperText> <FormHelperText>
<span> <span>
Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive) Comma separated list of words, phrases or hashtags you never want to see in notes. (case insensitive)

View File

@@ -13,10 +13,53 @@ import {
Input, Input,
Link, Link,
FormErrorMessage, FormErrorMessage,
Select,
Button,
Text,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useLocalStorage } from "react-use";
import { safeUrl } from "../../helpers/parse"; import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations"; import { AppSettings } from "../../services/settings/migrations";
import { PerformanceIcon } from "../../components/icons"; import { PerformanceIcon } from "../../components/icons";
import { selectedMethod } from "../../services/verify-event";
function VerifyEventSettings() {
const [verifyEventMethod, setVerifyEventMethod] = useLocalStorage<string>("verify-event-method", "default", {
raw: true,
});
console.log(selectedMethod, verifyEventMethod);
return (
<>
<FormControl>
<FormLabel htmlFor="verifyEventMethod" mb="0">
Verify event method
</FormLabel>
<Select value={verifyEventMethod} onChange={(e) => setVerifyEventMethod(e.target.value)} maxW="sm">
<option value="default">Default</option>
<option value="wasm">WebAssembly</option>
<option value="none">None</option>
</Select>
<FormHelperText>Default: All events signatures are checked</FormHelperText>
<FormHelperText>WebAssembly: Events signatures are checked in a separate thread</FormHelperText>
<FormHelperText>None: Only Profiles, Follows, and replaceable event signatures are checked</FormHelperText>
{selectedMethod !== verifyEventMethod && (
<>
<Text color="blue.500" mt="2">
NOTE: You must reload the app for this setting to take effect
</Text>
<Button colorScheme="primary" size="sm" onClick={() => location.reload()}>
Reload App
</Button>
</>
)}
</FormControl>
</>
);
}
export default function PerformanceSettings() { export default function PerformanceSettings() {
const { register, formState } = useFormContext<AppSettings>(); const { register, formState } = useFormContext<AppSettings>();
@@ -86,15 +129,6 @@ export default function PerformanceSettings() {
</Flex> </Flex>
<FormHelperText>Enabled: Show reactions on notes</FormHelperText> <FormHelperText>Enabled: Show reactions on notes</FormHelperText>
</FormControl> </FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="showSignatureVerification" mb="0">
Show signature verification
</FormLabel>
<Switch id="showSignatureVerification" {...register("showSignatureVerification")} />
</Flex>
<FormHelperText>Enabled: show signature verification on notes</FormHelperText>
</FormControl>
<FormControl> <FormControl>
<Flex alignItems="center"> <Flex alignItems="center">
<FormLabel htmlFor="autoDecryptDMs" mb="0"> <FormLabel htmlFor="autoDecryptDMs" mb="0">
@@ -104,6 +138,7 @@ export default function PerformanceSettings() {
</Flex> </Flex>
<FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText> <FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText>
</FormControl> </FormControl>
<VerifyEventSettings />
</Flex> </Flex>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>

View File

@@ -16,12 +16,13 @@ import {
useDisclosure, useDisclosure,
useToast, useToast,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, verifyEvent } from "nostr-tools";
import dayjs from "dayjs";
import VerticalPageLayout from "../../../components/vertical-page-layout"; import VerticalPageLayout from "../../../components/vertical-page-layout";
import BackButton from "../../../components/router/back-button"; import BackButton from "../../../components/router/back-button";
import Play from "../../../components/icons/play"; import Play from "../../../components/icons/play";
import EventEditor from "./event-editor"; import EventEditor from "./event-editor";
import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, verifyEvent } from "nostr-tools";
import dayjs from "dayjs";
import { processEvent } from "./process"; import { processEvent } from "./process";
import { WritingIcon } from "../../../components/icons"; import { WritingIcon } from "../../../components/icons";
import { useSigningContext } from "../../../providers/global/signing-provider"; import { useSigningContext } from "../../../providers/global/signing-provider";

View File

@@ -5473,10 +5473,10 @@ nostr-tools@^2.3.2:
optionalDependencies: optionalDependencies:
nostr-wasm v0.1.0 nostr-wasm v0.1.0
nostr-tools@^2.4.0: nostr-tools@^2.5.0:
version "2.4.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.4.0.tgz#bc2140a95ce0be8d4900bd852f652d811562753e" resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.0.tgz#083c8a22eb88c65f30d88a25e200ea2274348663"
integrity sha512-xQC7XdGeh0gLyprcKhvx5lwr7OQ+ZOiQ9C6GpzlVAj+EBv+AiN8kySb57t3uJoG1HK15oT9jf++MmQLwhp1xNQ== integrity sha512-G02O3JYNCfhx9NDjd3NOCw/5ck8PX5hiOIhHKpsXyu49ZtZbxGH3OLP9tf0fpUZ+EVWdjIYFR689sV0i7+TOng==
dependencies: dependencies:
"@noble/ciphers" "^0.5.1" "@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0" "@noble/curves" "1.2.0"
@@ -5487,7 +5487,7 @@ nostr-tools@^2.4.0:
optionalDependencies: optionalDependencies:
nostr-wasm v0.1.0 nostr-wasm v0.1.0
nostr-wasm@v0.1.0: nostr-wasm@^0.1.0, nostr-wasm@v0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94" resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA== integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==