mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix: make rumor wrap rebroadcast reachable in both audit edge cases
- Bare-seal hosts (kind 13) carry no p tag, so the re-download filter's p constraint silently matched nothing for them; the p filter is now wrap-only (the ids filter is sufficient for seals) - A just-sent private note has no relays until its self-wrap echoes back from the DM relays; the fetch now falls back to the account's own DM inbox relay set when note.relays is empty Also pins the citation guarantee with RumorHostCitationTest: a rumor note's nevent must encode the delivering wrap's id, never the private rumor id, and public notes keep citing their own id. https://claude.ai/code/session_01B39MQmrT3dz137nfpXABvo
This commit is contained in:
@@ -1333,20 +1333,28 @@ class Account(
|
||||
note.event?.let { noteEvent ->
|
||||
val host = note.rumorHost
|
||||
if (host != null) {
|
||||
// Rumors are rebroadcast as their delivering wrap:
|
||||
// download the wrap and send it.
|
||||
// Rumors are rebroadcast as their delivering envelope: the
|
||||
// cached copy is content-stripped, so download it and send it.
|
||||
// A just-sent note has no relays until its self-wrap echoes
|
||||
// back — fall back to our own DM inbox relays. Bare seals
|
||||
// (kind 13) carry no p tag, so that filter is wrap-only.
|
||||
val relays = note.relays.ifEmpty { dmRelays.flow.value.toList() }
|
||||
val filter =
|
||||
if (host.kind == SealedRumorEvent.KIND) {
|
||||
Filter(
|
||||
kinds = listOf(host.kind),
|
||||
ids = listOf(host.id),
|
||||
)
|
||||
} else {
|
||||
Filter(
|
||||
kinds = listOf(host.kind),
|
||||
tags = mapOf("p" to listOf(pubKey)),
|
||||
ids = listOf(host.id),
|
||||
)
|
||||
}
|
||||
client
|
||||
.fetchFirst(
|
||||
filters =
|
||||
note.relays.associateWith { _ ->
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(host.kind),
|
||||
tags = mapOf("p" to listOf(pubKey)),
|
||||
ids = listOf(host.id),
|
||||
),
|
||||
)
|
||||
},
|
||||
filters = relays.associateWith { _ -> listOf(filter) },
|
||||
)?.let { downloadedEvent ->
|
||||
val toRelays = computeRelayListToBroadcast(downloadedEvent)
|
||||
client.publish(downloadedEvent, toRelays)
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.unwrapAndUnsealOrNull
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Pins the citation guarantee for private notes: an nevent of a rumor must
|
||||
* encode the delivering envelope's id — never the rumor's own id, which is
|
||||
* the private event's identity and resolves to nothing on public relays.
|
||||
*/
|
||||
class RumorHostCitationTest {
|
||||
private val alicePriv = "0000000000000000000000000000000000000000000000000000000000000007"
|
||||
private val aliceSigner = NostrSignerInternal(KeyPair(alicePriv.hexToByteArray()))
|
||||
|
||||
private val bobPriv = "0000000000000000000000000000000000000000000000000000000000000008"
|
||||
private val bobSigner = NostrSignerInternal(KeyPair(bobPriv.hexToByteArray()))
|
||||
|
||||
@Test
|
||||
fun rumorNote_toNEvent_citesTheDeliveringWrap() =
|
||||
runTest {
|
||||
// Alice sends Bob a private note; Bob unwraps his copy.
|
||||
val template =
|
||||
TextNoteEvent.build("psst") {
|
||||
pTags(listOf(PTag(bobSigner.pubKey, null)))
|
||||
}
|
||||
val result = NIP17Factory().createNoteNIP17(template, aliceSigner)
|
||||
val bobWrap = result.wraps.first { it.recipientPubKey() == bobSigner.pubKey }
|
||||
val rumor = bobWrap.unwrapAndUnsealOrNull(bobSigner)
|
||||
assertNotNull(rumor)
|
||||
assertTrue(rumor.sig.isEmpty())
|
||||
|
||||
// Bob's cache materializes the rumor note and records the wrap.
|
||||
val note = Note(rumor.id)
|
||||
note.event = rumor
|
||||
note.recordRumorHost(bobWrap)
|
||||
|
||||
val nevent = note.toNEvent()
|
||||
assertEquals(
|
||||
NEvent.create(bobWrap.id, bobWrap.pubKey, bobWrap.kind, null),
|
||||
nevent,
|
||||
"rumor citations must encode the wrap, not the rumor",
|
||||
)
|
||||
assertFalse(
|
||||
nevent == NEvent.create(rumor.id, rumor.pubKey, rumor.kind, null),
|
||||
"the private rumor id must never be encoded",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publicNote_toNEvent_citesItsOwnId() =
|
||||
runTest {
|
||||
val event = aliceSigner.sign(TextNoteEvent.build("hello world"))
|
||||
|
||||
val note = Note(event.id)
|
||||
note.event = event
|
||||
|
||||
// toNEvent reads the author from the Note (unset here), so the
|
||||
// expected nevent carries a null author too.
|
||||
assertEquals(
|
||||
NEvent.create(event.id, null, event.kind, null),
|
||||
note.toNEvent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user