fix(nostr): unblock the signer handshake (priority + decrypt timeouts)

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-15 09:24:21 -04:00
1 parent ff1afa1175
commit 265b7813f2
4 files changed
+19 -7

No files matched your search

+10
View File
@@ -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;
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+7 -5
View File
@@ -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;