[watch-history] strictly format-route decrypt to halve NIP-46 RPCs

Watch-history decryption previously ordered the matching cipher family
first but kept the other family as a *fallback*. The opposite family can
never decrypt a given ciphertext (NIP-04 always carries a literal "?iv="
marker; NIP-44 is plain base64), so that fallback was a guaranteed-wasted
decrypt attempt. Under a NIP-46 remote signer every decrypt is a
*published* relay RPC routed through the serial 250ms request queue, so a
transient failure on the correct family triggered a second wasted RPC
that burned the queue and pushed cold loads toward the relay rate limit
("noting too much") — the symptom users hit.

determineWatchHistoryDecryptionOrder now filters the attempt set to the
single family implied by the ciphertext shape (trusting the ciphertext
over a possibly-misleading "encrypted" tag), with a safety fallback to
the full ordered list when nothing matches so the only available
decryptor is never stranded. This mirrors the list-decrypt routing
already shipped for userBlocks / subscriptions / hashtags (slice A).

Test: a NIP-44 ciphertext mislabeled with an ["encrypted","nip04"] tag,
against a signer exposing both decryptors where nip04 returns decoy data.
Asserts nip04 is never attempted and the nip44 plaintext is used.
Mutation-verified: removing the filter fails the test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-17 12:55:18 -04:00
1 parent d143883e9c
commit cd614876f5
2 files changed
+111 -1

No files matched your search

+33 -1
View File
@@ -677,7 +677,39 @@ function determineWatchHistoryDecryptionOrder(
}
}
return prioritized.length ? prioritized : available;
const ordered = prioritized.length ? prioritized : available;
// Strictly format-route by the ciphertext shape, which is authoritative.
// NIP-04 ciphertext always carries a literal "?iv=" marker; NIP-44 is plain
// base64. The opposite family can NEVER decrypt a given ciphertext, so trying
// it is a guaranteed-wasted decrypt — and under a NIP-46 remote signer each
// wasted decrypt is a *published* relay RPC that burns the serial request
// queue and risks the relay rate limit ("noting too much"). Dropping the
// non-matching family halves cold-load NIP-46 decrypt RPCs for watch history,
// mirroring the list-decrypt routing in userBlocks/subscriptions/hashtags.
// Trust the ciphertext over a possibly-misleading "encrypted" tag.
const normalizedCiphertext =
typeof ciphertext === "string" ? ciphertext.trim() : "";
if (normalizedCiphertext) {
const formatFamily = normalizedCiphertext.includes("?iv=")
? "nip04"
: "nip44";
const matching = ordered.filter(
(scheme) => watchHistorySchemeFamily(scheme) === formatFamily,
);
// Safety: never strand the only available decryptor. If nothing matches the
// detected format (e.g. a contrived/legacy payload), fall back to the full
// ordered list rather than returning an empty attempt set.
if (matching.length) {
return matching;
}
}
return ordered;
}
function watchHistorySchemeFamily(scheme) {
return scheme === "nip04" ? "nip04" : "nip44";
}
async function resolveNostrToolkit(deps = {}, cacheRef = null) {