Add marmot commit result
This commit is contained in:
1 parent
aa5e96caf2
commit
cb129bb0c6
5 files changed
+220
-52
No files matched your search
+15
@@ -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)
|
||||||
|
}
|
||||||
+67
-50
@@ -9,6 +9,7 @@ import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRela
|
|||||||
import at.torch.compose.database.model.ChatMessageNostrEventRelation
|
import at.torch.compose.database.model.ChatMessageNostrEventRelation
|
||||||
import at.torch.compose.database.model.ChatRoom
|
import at.torch.compose.database.model.ChatRoom
|
||||||
import at.torch.compose.database.model.GiftWrapPayload
|
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.MarmotGroupEvent
|
||||||
import at.torch.compose.database.model.MarmotInnerEvent
|
import at.torch.compose.database.model.MarmotInnerEvent
|
||||||
import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration
|
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.group.MlsGroup
|
||||||
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
||||||
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
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.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
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.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
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.nip59Giftwrap.rumors.RumorAssembler
|
||||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.io.bytestring.encode
|
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@@ -58,7 +56,6 @@ abstract class MarmotOutboundDao(
|
|||||||
peerKeyPackage: MarmotKeyPackage,
|
peerKeyPackage: MarmotKeyPackage,
|
||||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
||||||
): String {
|
): String {
|
||||||
// TODO: Might move this logic to a marmotOutboudDao for consistency...
|
|
||||||
val groupId = RandomInstance.bytes(32).toHexKey()
|
val groupId = RandomInstance.bytes(32).toHexKey()
|
||||||
val mlsGroup = MlsGroup.create(
|
val mlsGroup = MlsGroup.create(
|
||||||
identity = userPublicKey.hexToByteArray(),
|
identity = userPublicKey.hexToByteArray(),
|
||||||
@@ -91,10 +88,11 @@ abstract class MarmotOutboundDao(
|
|||||||
userPublicKey = userPublicKey,
|
userPublicKey = userPublicKey,
|
||||||
peerPublicKey = peerPublicKey,
|
peerPublicKey = peerPublicKey,
|
||||||
peerKeyPackage = peerKeyPackage,
|
peerKeyPackage = peerKeyPackage,
|
||||||
relays = relays
|
relays = relays,
|
||||||
|
isOneMemberInitialGroupCreation = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Update chatRoom
|
// Update chatRoom
|
||||||
database.chatRoomDao().upsert(
|
database.chatRoomDao().upsert(
|
||||||
chatRoom.copy(
|
chatRoom.copy(
|
||||||
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
|
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
|
||||||
@@ -110,7 +108,8 @@ abstract class MarmotOutboundDao(
|
|||||||
userPublicKey: HexKey,
|
userPublicKey: HexKey,
|
||||||
peerPublicKey: HexKey,
|
peerPublicKey: HexKey,
|
||||||
peerKeyPackage: MarmotKeyPackage,
|
peerKeyPackage: MarmotKeyPackage,
|
||||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
relays: List<String> = Relays.DefaultDMRelayList.map { it.url },
|
||||||
|
isOneMemberInitialGroupCreation: Boolean
|
||||||
) {
|
) {
|
||||||
val keyPackage = MlsKeyPackage.decodeTls(
|
val keyPackage = MlsKeyPackage.decodeTls(
|
||||||
TlsReader(
|
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(
|
database.broadcastNostrEventRequestDao().insert(
|
||||||
relays.map {
|
relays.map {
|
||||||
BroadcastNostrEventRequest(
|
BroadcastNostrEventRequest(
|
||||||
@@ -171,50 +186,52 @@ abstract class MarmotOutboundDao(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send welcome event to participant
|
if (isOneMemberInitialGroupCreation) {
|
||||||
commitResult.welcomeBytes?.let { welcomeBytes ->
|
// Send welcome event to participant
|
||||||
val welcomeBase64 = Base64.encode(
|
commitResult.welcomeBytes?.let { welcomeBytes ->
|
||||||
source = 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
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import kotlin.time.Instant
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
|
data class GiftWrapPayload(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val id: HexKey,
|
val id: HexKey,
|
||||||
|
|
||||||
|
|||||||
+136
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -99,7 +99,7 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
|
|||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "GiftWrapPayload"
|
const val TAG = "MarmotInnerEvent"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the NIP-40 expiration timestamp for outbound application messages,
|
* Compute the NIP-40 expiration timestamp for outbound application messages,
|
||||||
|
|||||||
Reference in new issue
Block a user