From 2bc6321d58a9788cc1697bbf45cb49617dda7905 Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 23 Sep 2026 10:06:11 -0500 Subject: [PATCH] fix(pairing): retry identity RPC; pairing set to proven relays only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 09:44 live scan proved the failure with the new accepted_by trace: our get_public_key was published and accepted, but Amber's own connect echo was created one second LATER — Amber's subscription to our client key opens milliseconds after we publish, so the first identity request is already in the relays' past when its listener starts (relays never replay history to a fresh subscription). The signer-side race is structural and unfixable from our subscription ordering; adopt_identity now retries get_public_key up to 4x with 3/8/15s backoff (non-timeout errors still fail fast), so a later attempt lands while the signer is listening. pairing_relays() narrowed to primal + nos.lol — the only two relays with proven bidirectional ephemeral-24133 traffic in today's scans. damus.io 503'd reads and silently dropped events; snort persists nothing; user settings switched to the two proven relays as well. --- src/relays.rs | 13 ++++++++----- src/signer/nip46_client.rs | 40 ++++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/relays.rs b/src/relays.rs index 1ed4dde..9964b03 100644 --- a/src/relays.rs +++ b/src/relays.rs @@ -27,15 +27,18 @@ pub fn default_relays() -> Vec { /// connect event is thrown away, so it must never be in the pairing URI. /// - wss://relay.nostr.band currently hangs the WebSocket handshake; it is /// also pay-to-read. Dropped from the pairing set. -/// - wss://nos.lol and wss://relay.snort.social accept ephemeral 24133 and -/// serve it live (snort does not persist ephemeral kinds, which is fine — -/// pairing only needs the live push). +/// - wss://nos.lol and wss://relay.primal.net are the two relays with +/// PROVEN bidirectional ephemeral-24133 traffic in the live Sep 23 scans +/// (both stored Amber's connect reply AND our identity RPC). +/// - wss://relay.damus.io returned HTTP 503 to reads and answered connects +/// inconsistently during the same scans; while flapping it splits the +/// conversation across relays the signer never reads. +/// - wss://relay.snort.social accepts ephemeral 24133 publishes but does +/// not persist them; with damus out it adds no shared ground. pub fn pairing_relays() -> Vec { vec![ - "wss://relay.damus.io".to_string(), "wss://relay.primal.net".to_string(), "wss://nos.lol".to_string(), - "wss://relay.snort.social".to_string(), ] } diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index 0ea98b3..dd5e404 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -1714,10 +1714,42 @@ impl Nip46ClientSigner { }; // Learn the REAL signing identity from the signer itself. - let identity = self - .send_rpc("get_public_key", vec![], REQUEST_TIMEOUT, false) - .await - .map_err(|e| format!("The signer would not reveal its public key: {e}"))?; + // + // RETRIED with backoff on timeout. The live Sep 23 scans proved a + // structural race on the SIGNER side: our first `get_public_key` + // publishes the instant the connect echo is verified, but Amber's + // own subscription to our client key opens milliseconds later, so + // the request is already in the relays' past when its listener + // starts — relays never replay history to a fresh subscription and + // the answer never comes. A retry lands while the signer is + // listening. Non-timeout failures (refusal, bad payload) still fail + // fast. + let mut attempt = 0u32; + let identity = loop { + attempt += 1; + match self + .send_rpc("get_public_key", vec![], REQUEST_TIMEOUT, false) + .await + { + Ok(identity) => break identity, + Err(e) => { + if !matches!(e, SigningError::Timeout) || attempt >= 4 { + return Err(format!("The signer would not reveal its public key: {e}")); + } + if live_relays(&connection.relays) { + pairing_trace(&format!( + "identity attempt {attempt} timed out; retrying get_public_key" + )); + } + tokio::time::sleep(Duration::from_secs(match attempt { + 1 => 3, + 2 => 8, + _ => 15, + })) + .await; + } + } + }; let identity = PublicKey::from_hex(identity.trim()) .map_err(|e| format!("The signer returned an unreadable public key: {e}"))?; let identity_npub = identity