fix(pairing): retry identity RPC; pairing set to proven relays only

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.
This commit is contained in:
Avi 2026-09-23 10:06:11 -05:00
commit 2bc6321d58
2 changed files with 44 additions and 9 deletions

View file

@ -27,15 +27,18 @@ pub fn default_relays() -> Vec<RelayConfig> {
/// connect event is thrown away, so it must never be in the pairing URI. /// 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 /// - wss://relay.nostr.band currently hangs the WebSocket handshake; it is
/// also pay-to-read. Dropped from the pairing set. /// also pay-to-read. Dropped from the pairing set.
/// - wss://nos.lol and wss://relay.snort.social accept ephemeral 24133 and /// - wss://nos.lol and wss://relay.primal.net are the two relays with
/// serve it live (snort does not persist ephemeral kinds, which is fine — /// PROVEN bidirectional ephemeral-24133 traffic in the live Sep 23 scans
/// pairing only needs the live push). /// (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<String> { pub fn pairing_relays() -> Vec<String> {
vec![ vec![
"wss://relay.damus.io".to_string(),
"wss://relay.primal.net".to_string(), "wss://relay.primal.net".to_string(),
"wss://nos.lol".to_string(), "wss://nos.lol".to_string(),
"wss://relay.snort.social".to_string(),
] ]
} }

View file

@ -1714,10 +1714,42 @@ impl Nip46ClientSigner {
}; };
// Learn the REAL signing identity from the signer itself. // Learn the REAL signing identity from the signer itself.
let identity = self //
.send_rpc("get_public_key", vec![], REQUEST_TIMEOUT, false) // RETRIED with backoff on timeout. The live Sep 23 scans proved a
.await // structural race on the SIGNER side: our first `get_public_key`
.map_err(|e| format!("The signer would not reveal its public key: {e}"))?; // 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()) let identity = PublicKey::from_hex(identity.trim())
.map_err(|e| format!("The signer returned an unreadable public key: {e}"))?; .map_err(|e| format!("The signer returned an unreadable public key: {e}"))?;
let identity_npub = identity let identity_npub = identity