fix: ask relays for gift wraps addressed to us, not to our peers

Three kind:1059 sync filters named the wrong pubkey.

ChatMessageListViewModel asked for `#p:[peer]` with no author constraint,
which subscribes to every wrap anyone has ever sent that peer. None of it is
decryptable by us, and it is the direct source of the Invalid Mac saves fixed
in the previous commit. It now asks for `#p:[us]` on our own DM relays — the
only shape of gift wrap filter that can return something we hold a key for.
The peer's relays were the wrong place to look regardless: under NIP-17 a
sender publishes to the *recipient's* DM relays, so our mail lands on ours.

The two in NostrDao asked for `authors:[userPublicKey]` + `#p:[participant]`,
commented "messages from this relay that were sent by us". A gift wrap is
signed by the throwaway key from GiftWrapEvent.create, never by the sender's
identity key, so no author value we could know will ever match one. These
requests were queued once per participant and always reconciled to empty —
failing silently rather than loudly, which is why they outlived the bug that
made the third filter visible. Both `if (chatMessageRelayListEvent != null)`
branches held nothing else, so each is inverted to the `== null` case that
does the real work: warn, and queue a profile sync for the participant whose
DM relay list we are missing. Nothing is lost; neither filter ever returned
an event.

Two things worth recording about what a filter can and cannot express here.

A wrap discloses only its recipient, so "the messages in this conversation"
is not askable — `#p:[us]` pulls the whole inbox and that is the narrowest
correct request. That is the privacy property being paid for, not a
limitation to work around.

Sent-message recovery is likewise not a filter problem. It needs a second
wrap addressed to ourselves at send time, which giftWrapAndBroadcast does not
yet emit; the `#p:[us]` filters already in place would pick those up with no
new subscription.

purpose on the chat message request changes from "sent-messages" to "chat",
matching the now-identical filter in ChatRoomListViewModel. Since computeId
buckets by minute and NegentropySynchronizeRequestDao upserts, the two
collapse into a single request rather than racing as separate rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-05 23:07:03 +02:00
parent f57644aa1f
commit f38a5f12f3
2 changed files with 16 additions and 68 deletions

View File

@@ -670,41 +670,11 @@ abstract class NostrDao(
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter =
SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair(
"p",
listOf(participant.participantPublicKey)
)
),
limit = 50
)
database.negentropySynchronizeRequestDao()
.insert(
chatMessageRelayListEvent.relays()
.map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
// Nothing to sync when we do have the relay list: a gift
// wrap is authored by a throwaway key, so authors=us matched
// nothing and this request was always empty. Our own inbox is
// synced by p-tag on the chat room list instead.
if (chatMessageRelayListEvent == null) {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
@@ -906,35 +876,9 @@ abstract class NostrDao(
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter = SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair("p", listOf(participant.participantPublicKey))
),
limit = 50
)
database.negentropySynchronizeRequestDao().insert(
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
// See the identical block above: authors=us never matches a
// gift wrap, so only the missing-relay-list case has work to do.
if (chatMessageRelayListEvent == null) {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...

View File

@@ -123,15 +123,19 @@ class ChatMessageListViewModel(
val chatMessageRelayListEvent = chatRepository.getChatMessageRelayForPublicKey(recipients.participant.participantPublicKey)
val relayAndSynchronizationFilter = if (chatMessageRelayListEvent != null) {
// Sync messages from this relay...
// Refresh our own inbox. A wrap names only its recipient, so
// "the messages in this conversation" is not something a filter
// can ask for, and the recipient's relays hold their mail, not
// ours. p-tagging the peer here fetched other people's wraps,
// which no key of ours can open.
Pair(
chatMessageRelayListEvent.relays(),
Relays.DefaultDMRelayList,
SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(recipients.participant.participantPublicKey))
Pair("p", listOf(localChatRoom.chatRoom.userPublicKey))
),
limit = 50
)
@@ -166,7 +170,7 @@ class ChatMessageListViewModel(
purpose = if (isReceiverChatMessageRelayListMissing.value) {
"chat-message-relays"
} else {
"sent-messages"
"chat"
},
synchronizationFilter = relayAndSynchronizationFilter.second,
relayURL = normalizedRelayUrl.url,