From 265b7813f21af6860e153e675de999f93d770220 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:24:21 -0400 Subject: [PATCH] fix(nostr): unblock the signer handshake (priority + decrypt timeouts) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-env console (2026-06-15) showed [signer-ready] taking ~48s and every list decrypt timing out before AND after it. Root mechanism: a priority inversion on the 2-slot NIP-07 request queue. - List decrypts run at NIP07_PRIORITY.HIGH; the readiness gate's permission/enable handshake ran at NORMAL. So a flood of HIGH-priority decrypts jumped ahead of the gate and starved it — the channel never got warmed, so the decrypts kept failing. Fix: add NIP07_PRIORITY.CRITICAL (above HIGH) and run the permission/enable handshake at CRITICAL so it always wins queue ordering. - Login-time decrypt timeouts were 12-15s even though a healthy nip04/nip44 decrypt is local crypto (~1s); the long window was only to tolerate a first-grant prompt, which the gate already pre-grants. A 15s timeout just monopolizes one of the two queue slots on a doomed call, blocking the gate for 15s at a time. Cut blocks/hashtags/subscriptions login decrypt timeouts to 6s so slots cycle ~2.5x faster (and the circuit breaker trips in ~18s instead of ~45s). Service-level 15s wrappers stay as a backstop (now strictly longer than the nip07 layer). Part of KNOWN_BUGS #0 ("cut the burst, then add a safety net"). Full unit suite green. Co-Authored-By: Claude Opus 4.8 --- js/nostr/nip07Permissions.js | 10 ++++++++++ js/services/hashtagPreferencesService.js | 2 +- js/subscriptions.js | 2 +- js/userBlocks.js | 12 +++++++----- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/js/nostr/nip07Permissions.js b/js/nostr/nip07Permissions.js index b96118c3..681fbbbf 100644 --- a/js/nostr/nip07Permissions.js +++ b/js/nostr/nip07Permissions.js @@ -35,6 +35,13 @@ export const DEFAULT_NIP07_PERMISSION_METHODS = Object.freeze([ ]); export const NIP07_PRIORITY = Object.freeze({ + // CRITICAL is reserved for establishing the signer channel itself (the + // permission/enable handshake behind the readiness gate). It must outrank + // list decryption (HIGH): otherwise a flood of HIGH-priority decrypts jumps + // ahead of the gate in the queue and starves it, so the channel never gets + // warmed and every decrypt keeps timing out (the real-env ~48s "signer-ready" + // — KNOWN_BUGS #0). + CRITICAL: 20, HIGH: 10, NORMAL: 5, LOW: 1, @@ -590,6 +597,9 @@ export async function requestEnablePermissions( ...variantTimeoutOverrides, // Interactive grant — never gated by (and can heal) the breaker. bypassCircuitBreaker: true, + // Establishing the channel must outrank list decryption so the + // handshake isn't starved by a flood of HIGH-priority decrypts. + priority: NIP07_PRIORITY.CRITICAL, }, ); return true; diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index bdc967e7..bfbf80f2 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -1094,7 +1094,7 @@ class HashtagPreferencesService { // gate guarantees the extension is ready before decryption starts, so // decrypt calls should complete within 1-2s. 5s accommodates slow // extensions while still failing fast for scheme fallback. - const nip07DecryptTimeoutMs = allowPermissionPrompt ? 12000 : SHORT_TIMEOUT_MS; + const nip07DecryptTimeoutMs = allowPermissionPrompt ? 6000 : SHORT_TIMEOUT_MS; const signerDecryptOptions = { priority: NIP07_PRIORITY.NORMAL, timeoutMs: nip07DecryptTimeoutMs, diff --git a/js/subscriptions.js b/js/subscriptions.js index b04e2a05..09d8c877 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1148,7 +1148,7 @@ class SubscriptionsManager { // decrypt calls should complete within 1-2s. 5s accommodates slow // extensions while still failing fast enough for scheme fallback. const nip07DecryptTimeoutMs = allowPermissionPrompt - ? 12000 + ? 6000 : SHORT_TIMEOUT_MS; const signerDecryptOptions = { priority: NIP07_PRIORITY.NORMAL, diff --git a/js/userBlocks.js b/js/userBlocks.js index c0f0316d..cc97e42b 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -953,11 +953,13 @@ class UserBlockListManager { : allowPermissionPrompt ? DECRYPT_TIMEOUT_MS : BACKGROUND_DECRYPT_TIMEOUT_MS; - // PERF: Previous 3s no-prompt timeout was too aggressive — extensions that - // have already granted permission still need time to decrypt. 8s gives a - // realistic window while still failing faster than the full 15s interactive - // timeout. - const nip07DecryptTimeoutMs = allowPermissionPrompt ? 15000 : 8000; + // A healthy nip04/nip44 decrypt is local crypto (~1s). The login-time value + // was 15s purely to tolerate a slow first-grant prompt — but permissions are + // pre-granted by the readiness gate, so a 15s timeout just monopolizes one of + // the two NIP-07 queue slots on a doomed call, starving the gate (real-env + // ~48s signer-ready — KNOWN_BUGS #0). 6s still tolerates a slow extension + // while freeing the slot ~2.5x faster. + const nip07DecryptTimeoutMs = allowPermissionPrompt ? 6000 : 6000; const emitStatus = (detail) => { if (!detail || typeof detail !== "object") { return;