Optimistic Verification
OptimisticEventVerifier is a drop-in verifyEvent callback for the Event Store that checks fewer signatures as relays prove they only deliver valid events. Every event still has its structure and id hash checked; only the Schnorr signature check is sampled.
Use it when signature verification is a real cost, such as large timeline or sync loads, and accepting some unchecked signatures is acceptable.
How it works
The verifier records successful signature checks for each relay in the event's seen relays. As a relay builds history, its trust grows from 0 to 1, and the chance of checking a signature falls from 100% to minimumVerificationRate.
- Trust requires both volume (
verifiedEventsForTrustchecks) and time (daysForTrustUTC days with at leastverifiedEventsPerDaychecks each) - Each day credits at most
verifiedEventsForTrust / daysForTrustchecks, so trust cannot be earned in one burst - Evidence older than
historyDaysexpires - Events seen on several relays use the strictest relay's sampling rate
- Events without seen relays are always fully verified
- An invalid event resets the evidence of every relay it was seen on
Optimistically accepted events never get the nostr-tools verifiedSymbol, so code that calls verifyEvent directly still checks the signature.
Usage
import { EventStore, OptimisticEventVerifier } from "applesauce-core";
const verifier = new OptimisticEventVerifier({ storage: localStorage });
const eventStore = new EventStore({ verifyEvent: verifier.verifyEvent });Options
These are the defaults:
const verifier = new OptimisticEventVerifier({
storage: localStorage, // optional, evidence is in-memory without it
minimumVerificationRate: 0.1, // lowest chance of checking a signature
verifiedEventsForTrust: 10_000, // checks for full volume trust
daysForTrust: 14, // qualifying days for full time trust
verifiedEventsPerDay: 20, // checks for a day to qualify
historyDays: 30, // evidence retention, at least daysForTrust
cacheSize: 10_000, // recently verified events to remember
});Counts must be positive safe integers and minimumVerificationRate must be greater than zero and at most one, otherwise the constructor throws a RangeError. A rate of 1 keeps every uncached signature check.
Persistence
The storage option accepts anything with string getItem and setItem methods, sync or async, so both localStorage and localForage work.
import localforage from "localforage";
const verifier = new OptimisticEventVerifier({ storage: localforage });Evidence loads lazily per relay and signatures are fully checked until it has loaded. Writes are batched in the background, and storage errors never block event ingestion. Instances sharing storage, such as other tabs, adopt a newer reset instead of overwriting it.
Observing trust
verifier.relay$(relay).subscribe((state) => {
console.log(state.trust, state.verificationProbability, state.history);
});getRelayState(url)returns a snapshot withready,trust,verificationProbability,verified, and dailyhistoryrelay$(url),trust$(url), andhistory$(url)update on new evidence, after loading, and at UTC day boundariesrelays$emits every relay URL the verifier has seenverifications$emits the outcome of every check:verified,skipped,cached, orinvalid
verifier.verifications$.pipe(filter((v) => v.result === "skipped")).subscribe(() => saved++);See the optimistic verification example for a live view of trust growth.
Integration
With RelayPool
Relays from applesauce-relay already add seen relays to events, so no extra wiring is needed:
pool.subscription(relays, filters).pipe(mapEventsToStore(eventStore)).subscribe();With other transports
Pass the source URL to add so the event has a seen relay; otherwise it is fully verified:
eventStore.add(event, "wss://relay.example.com");With multiple stores
Share one verifier so stores share evidence and skip repeat checks:
const eventStore = new EventStore({ verifyEvent: verifier.verifyEvent });
const asyncStore = new AsyncEventStore({ database, verifyEvent: verifier.verifyEvent });Best Practices
Keep full verification for high-value events
Sampling cannot guarantee authenticity. A trusted relay that turns malicious can get forged events accepted, and a forged replaceable event with a future created_at stays as the newest version. Wrap the verifier to always check profiles, contact lists, relay lists, and anything used for payments or authorization:
import { verifyEvent } from "nostr-tools";
const strict = new Set([0, 3, 10002]);
eventStore.verifyEvent = (event) => (strict.has(event.kind) ? verifyEvent(event) : verifier.verifyEvent(event));Only attach real provenance
Seen relays are the verifier's only trust input. Never copy them onto a different event or pass a relay URL to add that the event didn't come from.
Treat stored evidence as trusted input
Anything that can write to the storage can grant a relay trust. Avoid shared origins where other apps can write the same localStorage.
Deletes bypass verification
The Event Store handles kind 5 delete events before calling verifyEvent, so the verifier never sees them.