Add marmot commit result

This commit is contained in:
Kgothatso Ngako
2026-07-06 12:55:36 +02:00
parent aa5e96caf2
commit cb129bb0c6
5 changed files with 220 additions and 52 deletions

View File

@@ -0,0 +1,15 @@
package at.torch.compose.database.dao
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Upsert
import at.torch.compose.database.model.MarmotCommitResult
import at.torch.compose.database.model.MarmotKeyPackage
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import fr.acinq.bitcoin.PublicKey
@Dao
interface MarmotCommitResultDao {
@Upsert
suspend fun upsert(marmotCommitResult: MarmotCommitResult)
}

View File

@@ -9,6 +9,7 @@ import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRela
import at.torch.compose.database.model.ChatMessageNostrEventRelation
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.GiftWrapPayload
import at.torch.compose.database.model.MarmotCommitResult
import at.torch.compose.database.model.MarmotGroupEvent
import at.torch.compose.database.model.MarmotInnerEvent
import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration
@@ -27,12 +28,10 @@ import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
@@ -41,7 +40,6 @@ import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.io.bytestring.encode
import kotlin.io.encoding.Base64
import kotlin.time.Instant
@@ -58,7 +56,6 @@ abstract class MarmotOutboundDao(
peerKeyPackage: MarmotKeyPackage,
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
): String {
// TODO: Might move this logic to a marmotOutboudDao for consistency...
val groupId = RandomInstance.bytes(32).toHexKey()
val mlsGroup = MlsGroup.create(
identity = userPublicKey.hexToByteArray(),
@@ -91,10 +88,11 @@ abstract class MarmotOutboundDao(
userPublicKey = userPublicKey,
peerPublicKey = peerPublicKey,
peerKeyPackage = peerKeyPackage,
relays = relays
relays = relays,
isOneMemberInitialGroupCreation = true
)
// TODO: Update chatRoom
// Update chatRoom
database.chatRoomDao().upsert(
chatRoom.copy(
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
@@ -110,7 +108,8 @@ abstract class MarmotOutboundDao(
userPublicKey: HexKey,
peerPublicKey: HexKey,
peerKeyPackage: MarmotKeyPackage,
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
relays: List<String> = Relays.DefaultDMRelayList.map { it.url },
isOneMemberInitialGroupCreation: Boolean
) {
val keyPackage = MlsKeyPackage.decodeTls(
TlsReader(
@@ -162,6 +161,22 @@ abstract class MarmotOutboundDao(
)
)
// TODO: Save commitResult... in case we need to broadcast welcomeEvent after relay acknowledgement...
// database.marmotCommitResultDao().upsert(
// MarmotCommitResult(
// id = commitEvent.id,
// isOneMemberInitialGroupCreation = isOneMemberInitialGroupCreation,
// chatRoomId = groupId,
// commitBytes = commitResult.commitBytes,
// preCommitExporterSecret = commitResult.preCommitExporterSecret,
// welcomeBytes = commitResult.welcomeBytes,
// framedCommitBytes = commitResult.preCommitExporterSecret,
// groupInfoBytes = commitResult.groupInfoBytes,
// userPublicKey = userPublicKey,
// createdAt = Instant.fromEpochSeconds(commitEvent.createdAt)
// )
// )
database.broadcastNostrEventRequestDao().insert(
relays.map {
BroadcastNostrEventRequest(
@@ -171,50 +186,52 @@ abstract class MarmotOutboundDao(
}
)
// Send welcome event to participant
commitResult.welcomeBytes?.let { welcomeBytes ->
val welcomeBase64 = Base64.encode(
source = welcomeBytes
)
// GiftWrap...
val welcomeEvent = WelcomeEvent.build(
welcomeBase64 = welcomeBase64,
keyPackageEventId = peerKeyPackage.id,
relays = relays.map { NormalizedRelayUrl(it) },
nostrGroupId = groupId,
)
// Compute welcomeEventId
val welcomeEventId = EventHasher.hashId(
pubKey = userPublicKey,
kind = welcomeEvent.kind,
tags = welcomeEvent.tags,
createdAt = welcomeEvent.createdAt,
content = welcomeEvent.content
)
GiftWrapPayload(
id = welcomeEventId,
publicKey = userPublicKey,
chatRoomId = groupId,
kind = welcomeEvent.kind,
tags = welcomeEvent.tags,
content = welcomeEvent.content,
createdAt = Instant.fromEpochSeconds(welcomeEvent.createdAt)
)
// Save chatMessage for the invite...
database.chatMessageDao().upsert(
ChatMessage(
content = "Invited participant to chat", // use profile.humanReadable...
chatRoomId = groupId,
senderPublicKey = userPublicKey,
isUserMessage = true, // TODO: This is information message...
giftWrapPayloadId = welcomeEventId,
marmotGroupEventId = null,
marmotInnerEventId = null
if (isOneMemberInitialGroupCreation) {
// Send welcome event to participant
commitResult.welcomeBytes?.let { welcomeBytes ->
val welcomeBase64 = Base64.encode(
source = welcomeBytes
)
)
// GiftWrap...
val welcomeEvent = WelcomeEvent.build(
welcomeBase64 = welcomeBase64,
keyPackageEventId = peerKeyPackage.id,
relays = relays.map { NormalizedRelayUrl(it) },
nostrGroupId = groupId,
)
// Compute welcomeEventId
val welcomeEventId = EventHasher.hashId(
pubKey = userPublicKey,
kind = welcomeEvent.kind,
tags = welcomeEvent.tags,
createdAt = welcomeEvent.createdAt,
content = welcomeEvent.content
)
GiftWrapPayload(
id = welcomeEventId,
publicKey = userPublicKey,
chatRoomId = groupId,
kind = welcomeEvent.kind,
tags = welcomeEvent.tags,
content = welcomeEvent.content,
createdAt = Instant.fromEpochSeconds(welcomeEvent.createdAt)
)
// Save chatMessage for the invite...
database.chatMessageDao().upsert(
ChatMessage(
content = "Invited participant to chat", // use profile.humanReadable...
chatRoomId = groupId,
senderPublicKey = userPublicKey,
isUserMessage = true, // TODO: This is information message...
giftWrapPayloadId = welcomeEventId,
marmotGroupEventId = null,
marmotInnerEventId = null
)
)
}
}

View File

@@ -24,7 +24,7 @@ import kotlin.time.Instant
)
],
)
data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
data class GiftWrapPayload(
@PrimaryKey
val id: HexKey,

View File

@@ -0,0 +1,136 @@
package at.torch.compose.database.model
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.Ignore
import androidx.room3.PrimaryKey
import at.torch.compose.database.model.traits.LocalStoreEntity
import at.torch.compose.database.model.traits.SoftDeletableEntity
import at.torch.compose.database.model.traits.TimestampedEntity
import at.torch.compose.database.model.traits.UserViewableEntity
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEventEncryption
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip14Subject.subject
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Clock
import kotlin.time.Instant
@Entity(
foreignKeys = [
],
)
data class MarmotCommitResult( // TODO: Rename this to GiftWrapPayload...
@PrimaryKey
val id: HexKey,
val userPublicKey: HexKey,
val chatRoomId: HexKey,
val isOneMemberInitialGroupCreation: Boolean,
/**
* commitBytes is the raw TLS-encoded Commit struct (RFC 9420 §12.4)
*/
val commitBytes: ByteArray,
val welcomeBytes: ByteArray?,
val groupInfoBytes: ByteArray?,
/**
* Fully-framed commit ready for the MIP-03 outer ChaCha20 encryption.
* Wire format: MlsMessage(version=mls10, wireFormat=mls_public_message, payload=PublicMessage(...)).
*/
val framedCommitBytes: ByteArray = commitBytes,
/**
* `MLS-Exporter("marmot", "group-event", 32)` evaluated at the **pre-commit**
* epoch (N) — the key the group had when this commit was computed, before
* local state advanced to N+1. Publishers of the kind:445 MUST ChaCha20-wrap
* the commit with this key (RFC 9420 §12.4 and MDK parity). Using the
* post-commit (N+1) key makes the commit unreadable to existing members
* still at epoch N — the exact scenario that caused Eden to stall at
* epoch 1 and never decrypt any of David's subsequent messages.
*
* Empty by default for the test-only entry points that don't need it.
*/
val preCommitExporterSecret: ByteArray = ByteArray(0),
/**
* Current time
*/
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = createdAt,
override val savedAt: Instant = Clock.System.now(),
override val viewedAt: Instant? = null,
override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
): TimestampedEntity,
LocalStoreEntity,
UserViewableEntity,
SoftDeletableEntity {
@Ignore
private val logger = Logger.withTag(TAG)
companion object {
const val TAG = "MarmotCommitResult"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as MarmotCommitResult
if (isOneMemberInitialGroupCreation != other.isOneMemberInitialGroupCreation) return false
if (id != other.id) return false
if (userPublicKey != other.userPublicKey) return false
if (chatRoomId != other.chatRoomId) return false
if (!commitBytes.contentEquals(other.commitBytes)) return false
if (!welcomeBytes.contentEquals(other.welcomeBytes)) return false
if (!groupInfoBytes.contentEquals(other.groupInfoBytes)) return false
if (!framedCommitBytes.contentEquals(other.framedCommitBytes)) return false
if (!preCommitExporterSecret.contentEquals(other.preCommitExporterSecret)) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
if (savedAt != other.savedAt) return false
if (viewedAt != other.viewedAt) return false
if (deletedAt != other.deletedAt) return false
if (logger != other.logger) return false
return true
}
override fun hashCode(): Int {
var result = isOneMemberInitialGroupCreation.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + userPublicKey.hashCode()
result = 31 * result + chatRoomId.hashCode()
result = 31 * result + commitBytes.contentHashCode()
result = 31 * result + (welcomeBytes?.contentHashCode() ?: 0)
result = 31 * result + (groupInfoBytes?.contentHashCode() ?: 0)
result = 31 * result + framedCommitBytes.contentHashCode()
result = 31 * result + preCommitExporterSecret.contentHashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()
result = 31 * result + savedAt.hashCode()
result = 31 * result + (viewedAt?.hashCode() ?: 0)
result = 31 * result + (deletedAt?.hashCode() ?: 0)
result = 31 * result + logger.hashCode()
return result
}
}

View File

@@ -99,7 +99,7 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
companion object {
const val TAG = "GiftWrapPayload"
const val TAG = "MarmotInnerEvent"
/**
* Compute the NIP-40 expiration timestamp for outbound application messages,