fix(quartz): align NIP-53 Live Activities with spec
- MeetingRoomPresenceEvent (kind 10312) now extends BaseReplaceableEvent
instead of BaseAddressableEvent. NIP-53 states this kind is a regular
replaceable event ("presence can only be indicated in one room at a
time"), and LocalCache already routes it through consumeBaseReplaceable.
createAddress/createAddressATag/createAddressTag now take only pubKey.
- Add TagArrayBuilder extensions for streaming (30311), meeting space
(30312) and meeting room (30313) tags, plus build() helpers that assemble
the required tags per NIP-53.
- Add ProofOfAgreement utility to sign and verify the schnorr proof that
NIP-53 places in the 5th element of a participant p-tag
(SHA256 of kind:pubkey:dTag signed by the participant's private key).
Includes LiveActivitiesEvent.hasValidProof() convenience.
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.quartz.nip53LiveActivities.meetingSpaces
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag as StreamStatusTag
|
||||
|
||||
class MeetingSpaceEventBuildTest {
|
||||
private val host = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
@Test
|
||||
fun meetingSpaceBuildAssemblesRequiredTags() {
|
||||
val template =
|
||||
MeetingSpaceEvent.build(
|
||||
room = "Main Hall",
|
||||
status = StatusTag.STATUS.OPEN,
|
||||
service = "https://meet.example.com/hall",
|
||||
host = ParticipantTag(host, null, "Host", null),
|
||||
dTag = "main-hall",
|
||||
createdAt = 1_700_000_000L,
|
||||
) {
|
||||
summary("The big room")
|
||||
image("https://example.com/hall.png")
|
||||
endpoint("https://api.example.com/hall")
|
||||
}
|
||||
|
||||
assertEquals(MeetingSpaceEvent.KIND, template.kind)
|
||||
val byName = template.tags.groupBy { it[0] }
|
||||
|
||||
assertEquals("main-hall", byName["d"]?.single()?.get(1))
|
||||
assertEquals("Main Hall", byName["room"]?.single()?.get(1))
|
||||
assertEquals("open", byName["status"]?.single()?.get(1))
|
||||
assertEquals("https://meet.example.com/hall", byName["service"]?.single()?.get(1))
|
||||
assertEquals("https://api.example.com/hall", byName["endpoint"]?.single()?.get(1))
|
||||
assertEquals("The big room", byName["summary"]?.single()?.get(1))
|
||||
assertEquals("https://example.com/hall.png", byName["image"]?.single()?.get(1))
|
||||
assertEquals(MeetingSpaceEvent.ALT, byName["alt"]?.single()?.get(1))
|
||||
|
||||
val hostTag = byName["p"]?.single()
|
||||
assertNotNull(hostTag)
|
||||
assertEquals(host, hostTag[1])
|
||||
assertEquals("Host", hostTag[3])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun meetingRoomBuildReferencesParentSpace() {
|
||||
val spaceAddress = Address(MeetingSpaceEvent.KIND, host, "main-hall")
|
||||
val template =
|
||||
MeetingRoomEvent.build(
|
||||
meetingSpace = MeetingSpaceTag(spaceAddress),
|
||||
title = "Annual Meeting",
|
||||
starts = 1_700_000_000L,
|
||||
status = StreamStatusTag.STATUS.PLANNED,
|
||||
dTag = "annual-2026",
|
||||
createdAt = 1_700_000_000L,
|
||||
) {
|
||||
ends(1_700_003_600L)
|
||||
currentParticipants(10)
|
||||
totalParticipants(100)
|
||||
}
|
||||
|
||||
assertEquals(MeetingRoomEvent.KIND, template.kind)
|
||||
val byName = template.tags.groupBy { it[0] }
|
||||
|
||||
assertEquals("annual-2026", byName["d"]?.single()?.get(1))
|
||||
assertEquals("Annual Meeting", byName["title"]?.single()?.get(1))
|
||||
assertEquals("1700000000", byName["starts"]?.single()?.get(1))
|
||||
assertEquals("1700003600", byName["ends"]?.single()?.get(1))
|
||||
assertEquals("planned", byName["status"]?.single()?.get(1))
|
||||
assertEquals("10", byName["current_participants"]?.single()?.get(1))
|
||||
assertEquals("100", byName["total_participants"]?.single()?.get(1))
|
||||
assertEquals(MeetingRoomEvent.ALT, byName["alt"]?.single()?.get(1))
|
||||
|
||||
val aTag = byName["a"]?.single()
|
||||
assertNotNull(aTag)
|
||||
assertEquals("${MeetingSpaceEvent.KIND}:$host:main-hall", aTag[1])
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.quartz.nip53LiveActivities.presence
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MeetingRoomPresenceEventTest {
|
||||
private val participant = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
@Test
|
||||
fun kind10312IsReplaceable() {
|
||||
val event =
|
||||
MeetingRoomPresenceEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = participant,
|
||||
createdAt = 1_700_000_000L,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("a", "30313:${"b".repeat(64)}:annual-2026"),
|
||||
arrayOf("hand", "1"),
|
||||
),
|
||||
content = "",
|
||||
sig = "0".repeat(128),
|
||||
)
|
||||
|
||||
// Per NIP-53, kind 10312 is a regular replaceable event: "presence can only
|
||||
// be indicated in one room at a time". Replaceable events have a fixed empty
|
||||
// d-tag regardless of what's in the event tags.
|
||||
assertEquals("", event.dTag())
|
||||
assertEquals("${MeetingRoomPresenceEvent.KIND}:$participant:", event.addressTag())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createAddressKeysByPubKeyOnly() {
|
||||
val a = MeetingRoomPresenceEvent.createAddress(participant)
|
||||
assertEquals(MeetingRoomPresenceEvent.KIND, a.kind)
|
||||
assertEquals(participant, a.pubKeyHex)
|
||||
assertEquals("", a.dTag)
|
||||
|
||||
val aTag = MeetingRoomPresenceEvent.createAddressATag(participant)
|
||||
assertEquals(MeetingRoomPresenceEvent.KIND, aTag.kind)
|
||||
assertEquals(participant, aTag.pubKeyHex)
|
||||
assertEquals("", aTag.dTag)
|
||||
|
||||
assertEquals(
|
||||
"${MeetingRoomPresenceEvent.KIND}:$participant:",
|
||||
MeetingRoomPresenceEvent.createAddressTag(participant),
|
||||
)
|
||||
}
|
||||
}
|
||||
+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.quartz.nip53LiveActivities.streaming
|
||||
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LiveActivitiesEventBuildTest {
|
||||
private val host = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
private val guest = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
@Test
|
||||
fun buildAssemblesAllRequiredAndOptionalTags() {
|
||||
val template =
|
||||
LiveActivitiesEvent.build(dTag = "my-stream", createdAt = 1_700_000_000L) {
|
||||
title("My Stream")
|
||||
summary("A live event")
|
||||
image("https://example.com/img.png")
|
||||
streaming("https://example.com/stream.m3u8")
|
||||
starts(1_700_000_000L)
|
||||
status(StatusTag.STATUS.LIVE)
|
||||
currentParticipants(42)
|
||||
totalParticipants(100)
|
||||
participant(host, role = "Host")
|
||||
participant(guest, role = "Speaker")
|
||||
}
|
||||
|
||||
assertEquals(LiveActivitiesEvent.KIND, template.kind)
|
||||
assertEquals("", template.content)
|
||||
|
||||
val tagsByName = template.tags.groupBy { it[0] }
|
||||
|
||||
assertEquals("my-stream", tagsByName["d"]?.single()?.get(1))
|
||||
assertEquals("My Stream", tagsByName["title"]?.single()?.get(1))
|
||||
assertEquals("A live event", tagsByName["summary"]?.single()?.get(1))
|
||||
assertEquals("https://example.com/img.png", tagsByName["image"]?.single()?.get(1))
|
||||
assertEquals("https://example.com/stream.m3u8", tagsByName["streaming"]?.single()?.get(1))
|
||||
assertEquals("1700000000", tagsByName["starts"]?.single()?.get(1))
|
||||
assertEquals("live", tagsByName["status"]?.single()?.get(1))
|
||||
assertEquals("42", tagsByName["current_participants"]?.single()?.get(1))
|
||||
assertEquals("100", tagsByName["total_participants"]?.single()?.get(1))
|
||||
assertEquals(LiveActivitiesEvent.ALT, tagsByName["alt"]?.single()?.get(1))
|
||||
|
||||
// Per NIP-53, role lives at index 3 of the p-tag; a missing relay must be
|
||||
// represented as an empty string placeholder, not dropped.
|
||||
val pTags = tagsByName["p"] ?: emptyList()
|
||||
assertEquals(2, pTags.size)
|
||||
assertContentEquals(arrayOf("p", host, "", "Host"), pTags[0])
|
||||
assertContentEquals(arrayOf("p", guest, "", "Speaker"), pTags[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildAllowsMultiplePinnedAndHashtagsWithoutClobbering() {
|
||||
val template =
|
||||
LiveActivitiesEvent.build(dTag = "my-stream") {
|
||||
title("title")
|
||||
pinned("1".repeat(64))
|
||||
pinned("2".repeat(64))
|
||||
}
|
||||
|
||||
val pinned = template.tags.filter { it[0] == "pinned" }
|
||||
assertEquals(2, pinned.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildUsesRandomDTagWhenNotProvided() {
|
||||
val a = LiveActivitiesEvent.build { title("a") }
|
||||
val b = LiveActivitiesEvent.build { title("b") }
|
||||
val aDTag = a.tags.first { it[0] == "d" }[1]
|
||||
val bDTag = b.tags.first { it[0] == "d" }[1]
|
||||
assertNotNull(aDTag)
|
||||
assertTrue(aDTag.isNotEmpty())
|
||||
assertTrue(aDTag != bDTag, "random dTag should differ between builds")
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.quartz.nip53LiveActivities.streaming
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ProofOfAgreementTest {
|
||||
private val dummySig = "0".repeat(128)
|
||||
|
||||
@Test
|
||||
fun roundTripSignAndVerify() {
|
||||
val host = KeyPair()
|
||||
val participant = KeyPair()
|
||||
val hostHex = host.pubKey.toHexKey()
|
||||
val participantHex = participant.pubKey.toHexKey()
|
||||
val dTag = "my-stream"
|
||||
|
||||
val proof =
|
||||
ProofOfAgreement.sign(
|
||||
participantPrivKey = participant.privKey!!,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = dTag,
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
ProofOfAgreement.verify(
|
||||
proof = proof,
|
||||
participantPubKey = participantHex,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = dTag,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyFailsForDifferentAddress() {
|
||||
val host = KeyPair()
|
||||
val participant = KeyPair()
|
||||
val hostHex = host.pubKey.toHexKey()
|
||||
val participantHex = participant.pubKey.toHexKey()
|
||||
|
||||
val proof =
|
||||
ProofOfAgreement.sign(
|
||||
participantPrivKey = participant.privKey!!,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = "one",
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
ProofOfAgreement.verify(
|
||||
proof = proof,
|
||||
participantPubKey = participantHex,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = "another",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyFailsForWrongParticipantKey() {
|
||||
val host = KeyPair()
|
||||
val participant = KeyPair()
|
||||
val imposter = KeyPair()
|
||||
val hostHex = host.pubKey.toHexKey()
|
||||
|
||||
val proof =
|
||||
ProofOfAgreement.sign(
|
||||
participantPrivKey = participant.privKey!!,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = "stream",
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
ProofOfAgreement.verify(
|
||||
proof = proof,
|
||||
participantPubKey = imposter.pubKey.toHexKey(),
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = "stream",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyFailsForMalformedProof() {
|
||||
assertFalse(
|
||||
ProofOfAgreement.verify(
|
||||
proof = "not-hex",
|
||||
participantPubKey = "a".repeat(64),
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = "a".repeat(64),
|
||||
eventDTag = "x",
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
ProofOfAgreement.verify(
|
||||
proof = "ab",
|
||||
participantPubKey = "a".repeat(64),
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = "a".repeat(64),
|
||||
eventDTag = "x",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyAgainstLiveActivitiesEvent() {
|
||||
val host = KeyPair()
|
||||
val participant = KeyPair()
|
||||
val hostHex = host.pubKey.toHexKey()
|
||||
val participantHex = participant.pubKey.toHexKey()
|
||||
val dTag = "event-d"
|
||||
|
||||
val proof =
|
||||
ProofOfAgreement.sign(
|
||||
participantPrivKey = participant.privKey!!,
|
||||
eventKind = LiveActivitiesEvent.KIND,
|
||||
eventAuthor = hostHex,
|
||||
eventDTag = dTag,
|
||||
)
|
||||
|
||||
val event =
|
||||
LiveActivitiesEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = hostHex,
|
||||
createdAt = 1_700_000_000L,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", dTag),
|
||||
arrayOf("p", participantHex, "", "Speaker", proof),
|
||||
arrayOf("p", "c".repeat(64), "", "Speaker"),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
|
||||
val verifiedTag = event.participants().first { it.pubKey == participantHex }
|
||||
val unverifiedTag = event.participants().first { it.pubKey == "c".repeat(64) }
|
||||
|
||||
assertTrue(event.hasValidProof(verifiedTag))
|
||||
assertFalse(event.hasValidProof(unverifiedTag), "missing proof must not be treated as valid")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun digestMatchesAddressForm() {
|
||||
// digest(kind, pubkey, dtag) must agree with digest(Address)
|
||||
val hostHex = "a".repeat(64)
|
||||
val d1 = ProofOfAgreement.digest(LiveActivitiesEvent.KIND, hostHex, "abc")
|
||||
val d2 = ProofOfAgreement.digest(Address(LiveActivitiesEvent.KIND, hostHex, "abc"))
|
||||
assertTrue(d1.contentEquals(d2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tagWithoutProofFailsVerification() {
|
||||
val host = KeyPair()
|
||||
val participant = KeyPair()
|
||||
|
||||
val event =
|
||||
LiveActivitiesEvent(
|
||||
id = "0".repeat(64),
|
||||
pubKey = host.pubKey.toHexKey(),
|
||||
createdAt = 1_700_000_000L,
|
||||
tags = arrayOf(arrayOf("d", "d"), arrayOf("p", participant.pubKey.toHexKey())),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
ProofOfAgreement.verify(
|
||||
tag = ParticipantTag(participant.pubKey.toHexKey(), null, null, null),
|
||||
event = event,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user