mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix(relay): show relay groups when NIP-11 is unreachable; reconnect on trust change
Two fixes for the "connected over clearnet, receiving 39000s, but nothing shows" case on a Cloudflare-fronted relay (blocks the plain HTTP NIP-11 GET while serving events over the socket): - Relay group-list screen no longer hard-depends on NIP-11. The display gate needs the relay's `self` key (from NIP-11) to show relay-signed groups; when the NIP-11 fetch fails (FAIL_TO_REACH_SERVER — Cloudflare resets the GET), fall back to the dominant 39000 signer on that relay as its de-facto signer, so its groups render while a stray user-published 39000 (different author) stays filtered. Also re-fetch NIP-11 when the relay is marked Trusted (moved to clearnet), busting the cached over-Tor error (new Nip11CachedRetriever.invalidate). - RelayProxyClientConnector now reconnects the transport-flipped relay immediately when a relay-classification set changes (e.g. marking it Trusted → clearnet). Previously only TorRelaySettings changes counted, so a newly-trusted relay sat out its Tor-earned backoff before re-dialing. Scoped to onlyIfChanged (no resetBackoff), so only the flipped relay skips its delay and the rest of the pool's backoff is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
This commit is contained in:
+12
@@ -36,6 +36,18 @@ class Nip11CachedRetriever(
|
||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
|
||||
private val retriever = Nip11Retriever(okHttpClient)
|
||||
|
||||
/**
|
||||
* Drops any cached NIP-11 document (including a cached *error*) for [relay], so the next
|
||||
* [loadRelayInfo] performs a fresh fetch. Used when the transport to a relay changes — e.g. it's
|
||||
* marked Trusted to move off Tor onto clearnet — so a doc that failed over the old transport
|
||||
* (403/timeout) isn't served from cache for the rest of its TTL, which would keep the relay's
|
||||
* `self` key unknown and hide its relay-signed NIP-29 groups.
|
||||
*/
|
||||
fun invalidate(relay: NormalizedRelayUrl) {
|
||||
relayInformationDocumentCache.remove(relay)
|
||||
relayInformationEmptyCache.remove(relay)
|
||||
}
|
||||
|
||||
fun trimToSize(maxItems: Int) {
|
||||
relayInformationDocumentCache.trimToSize(maxItems)
|
||||
// relayInformationEmptyCache holds only lightweight display-name+favicon-url placeholders;
|
||||
|
||||
+33
-2
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -85,6 +86,16 @@ class RelayProxyClientConnector(
|
||||
// one relay serve out its delay.
|
||||
private var lastTorSettings: TorRelaySettings? = null
|
||||
|
||||
// The relay-classification sets (trusted/DM/money) from the last apply(). A relay moving into or
|
||||
// out of one of these flips its transport (e.g. marking a relay Trusted moves it off Tor onto
|
||||
// clearnet) WITHOUT changing TorRelaySettings, so `torPolicyChanged` above misses it and the
|
||||
// flipped relay would sit out its (now-irrelevant) backoff. We track these so such a relay can
|
||||
// skip its retry delay on the next reconnect — scoped to onlyIfChanged, so only the relays that
|
||||
// actually flipped re-dial and the rest of the pool's backoff is left untouched.
|
||||
private var lastTrustedRelays: Set<NormalizedRelayUrl>? = null
|
||||
private var lastDmRelays: Set<NormalizedRelayUrl>? = null
|
||||
private var lastMoneyOpRelays: Set<NormalizedRelayUrl>? = null
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val relayServices =
|
||||
combine(
|
||||
@@ -150,6 +161,9 @@ class RelayProxyClientConnector(
|
||||
lastTorSettings = torSettings
|
||||
lastTorConnection = infra.torConnection
|
||||
lastClearConnection = infra.clearConnection
|
||||
lastTrustedRelays = infra.evaluator.trustedRelayList
|
||||
lastDmRelays = infra.evaluator.dmRelayList
|
||||
lastMoneyOpRelays = infra.evaluator.moneyOpRelayList
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -170,12 +184,28 @@ class RelayProxyClientConnector(
|
||||
// relays go through Tor. The relays whose transport flipped must re-dial now.
|
||||
val torPolicyChanged = lastTorSettings != null && torSettings != lastTorSettings
|
||||
|
||||
// A relay moved into/out of the trusted/DM/money sets (e.g. marked Trusted to move it
|
||||
// off Tor onto clearnet). That flips only that relay's transport, not TorRelaySettings,
|
||||
// so let onlyIfChanged pick out the flipped relay(s) and skip THEIR retry delay —
|
||||
// without resetBackoff(), so the rest of the pool's backoff is untouched (these sets
|
||||
// churn while relay lists load, and forgiving the whole pool then would be too much).
|
||||
val classificationChanged =
|
||||
lastTrustedRelays != null &&
|
||||
(
|
||||
infra.evaluator.trustedRelayList != lastTrustedRelays ||
|
||||
infra.evaluator.dmRelayList != lastDmRelays ||
|
||||
infra.evaluator.moneyOpRelayList != lastMoneyOpRelays
|
||||
)
|
||||
|
||||
val previousNetworkId = lastNetworkId
|
||||
|
||||
lastTorConnection = infra.torConnection
|
||||
lastClearConnection = infra.clearConnection
|
||||
lastNetworkId = networkId ?: lastNetworkId
|
||||
lastTorSettings = torSettings
|
||||
lastTrustedRelays = infra.evaluator.trustedRelayList
|
||||
lastDmRelays = infra.evaluator.dmRelayList
|
||||
lastMoneyOpRelays = infra.evaluator.moneyOpRelayList
|
||||
|
||||
if (networkChanged) {
|
||||
Log.d("ManageRelayServices") {
|
||||
@@ -196,11 +226,12 @@ class RelayProxyClientConnector(
|
||||
|
||||
Log.d("ManageRelayServices") {
|
||||
"Relay Services have changed, reconnecting relays that need to " +
|
||||
"(transportChanged=$transportChanged torPolicyChanged=$torPolicyChanged)"
|
||||
"(transportChanged=$transportChanged torPolicyChanged=$torPolicyChanged " +
|
||||
"classificationChanged=$classificationChanged)"
|
||||
}
|
||||
client.reconnect(
|
||||
onlyIfChanged = true,
|
||||
ignoreRetryDelays = freshStart,
|
||||
ignoreRetryDelays = freshStart || classificationChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+36
-8
@@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.looksLikeNonNip29Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -113,9 +112,21 @@ fun RelayGroupChannelListScreen(
|
||||
|
||||
RelayGroupsOnRelaySubscription(relay, accountViewModel.dataSources().relayGroupsOnRelay, accountViewModel)
|
||||
|
||||
// Trust state drives the Tor→clearnet hint below AND a NIP-11 re-fetch: marking a relay Trusted
|
||||
// moves it off Tor onto clearnet, so a NIP-11 doc that failed over Tor must be re-fetched — its
|
||||
// cached error would otherwise keep the relay's `self` unknown for the whole TTL.
|
||||
val trustedRelays by accountViewModel.account.trustedRelayList.flow
|
||||
.collectAsStateWithLifecycle()
|
||||
val isTrusted = relay in trustedRelays
|
||||
|
||||
// Warm the relay's NIP-11 so we can tell its genuine (relay-signed) groups from stray
|
||||
// user-published 39000s that a non-NIP-29 relay may also be storing.
|
||||
val relayInfo by loadRelayInfo(relay)
|
||||
// user-published 39000s that a non-NIP-29 relay may also be storing. Re-keyed on trust so a move
|
||||
// to clearnet re-fetches over the new transport instead of serving the cached over-Tor failure.
|
||||
val nip11Cache = Amethyst.instance.nip11Cache
|
||||
val relayInfo by produceState(nip11Cache.getFromCache(relay), relay, isTrusted) {
|
||||
if (isTrusted) nip11Cache.invalidate(relay)
|
||||
nip11Cache.loadRelayInfo(relay, onInfo = { value = it }, onError = { _, _, _ -> })
|
||||
}
|
||||
|
||||
// Re-read the relay's channels whenever a group-metadata (kind 39000) event lands in
|
||||
// the cache — driven by LocalCache.observeEvents rather than a timer, so the list
|
||||
@@ -132,9 +143,28 @@ fun RelayGroupChannelListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Only the relay's own genuine, relay-signed groups (39000 author == the relay's NIP-11 `self`).
|
||||
// Recomputes as the NIP-11 doc resolves so real groups fill in and fakes stay hidden.
|
||||
val channels = remember(allChannels, relayInfo) { allChannels.filter { isRelaySignedRelayGroup(it, relayInfo) } }
|
||||
// Prefer the relay's own genuine, relay-signed groups (39000 author == the NIP-11 `self`).
|
||||
// Recomputes as the NIP-11 doc resolves so real groups fill in and fakes stay hidden. But if
|
||||
// NIP-11 is unreachable (e.g. a Cloudflare-fronted relay that resets the plain HTTP GET while
|
||||
// still serving events over the socket), fall back to the dominant 39000 signer on this relay as
|
||||
// its de-facto signer — so its relay-signed groups still show while a stray user-published 39000
|
||||
// (a different author) stays filtered.
|
||||
val channels =
|
||||
remember(allChannels, relayInfo) {
|
||||
val nip11Known = relayInfo.self != null || relayInfo.supported_nips != null
|
||||
if (nip11Known) {
|
||||
allChannels.filter { isRelaySignedRelayGroup(it, relayInfo) }
|
||||
} else {
|
||||
val dominantSigner =
|
||||
allChannels
|
||||
.mapNotNull { it.event?.pubKey }
|
||||
.groupingBy { it }
|
||||
.eachCount()
|
||||
.maxByOrNull { it.value }
|
||||
?.key
|
||||
if (dominantSigner != null) allChannels.filter { it.event?.pubKey == dominantSigner } else allChannels
|
||||
}
|
||||
}
|
||||
|
||||
// Buzz relays expose no public group directory (membership is server-side), so `channels` above
|
||||
// stays empty for them. When this is a Buzz relay, fold in the membership-scoped channels
|
||||
@@ -153,8 +183,6 @@ fun RelayGroupChannelListScreen(
|
||||
// Relay List (connected over clearnet even while Tor stays on for everything else).
|
||||
val torType by Amethyst.instance.torPrefs.torType
|
||||
.collectAsStateWithLifecycle(TorType.OFF)
|
||||
val trustedRelays by accountViewModel.account.trustedRelayList.flow
|
||||
.collectAsStateWithLifecycle()
|
||||
val isOnion = remember(relay) { relay.url.contains(".onion") }
|
||||
var connectTimedOut by remember(relay) { mutableStateOf(false) }
|
||||
LaunchedEffect(relay) {
|
||||
|
||||
Reference in New Issue
Block a user