Merge branch 'mantra' into claude/happy-gauss-dbe258

This commit is contained in:
Kgothatso Ngako committed 2026-09-06 15:06:13 +02:00
commit 54091099a9
18 files changed
+858 -377

No files matched your search

@@ -1,144 +0,0 @@
package press.mantra.compose.database.model
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import press.mantra.compose.nostr.nip30303.ArtifactEvent
import press.mantra.compose.nostr.nip30303.tags.ArtifactVersionMetadataTag
/**
* The first version of an artifact is derived, not delivered.
*
* The group signs an artifact and nothing else, so the version it starts life
* with is not an event anybody sent: every device builds the row for itself out
* of the artifact it already holds. That only works while every device builds
* the *same* row, and nothing about the ids would show it if they stopped —
* they are content hashes, opaque hex either way. What would show is a group
* that quietly disagrees about which version a chapter hangs off, with the
* artifact looking identical on every screen.
*/
class InitialArtifactVersionTest {
private val groupKey = "a".repeat(64)
private val dialectId = "b".repeat(64)
private val chatRoomId = "room"
private fun signedArtifact(
name: String = "In Detention",
versionLabel: String = "1.0",
createdAt: Long = 1_700_000_000,
): ArtifactEvent {
val template = ArtifactEvent.build(
name = name,
url = "example.com",
visibility = "private",
license = "cc",
dialectId = dialectId,
versionLabel = versionLabel,
createdAt = createdAt,
)
// Hashed rather than made up, so two fixtures that differ are two
// different artifacts here for the same reason they would be in the app.
return ArtifactEvent(
id = EventHasher.hashId(
pubKey = groupKey,
createdAt = template.createdAt,
kind = template.kind,
tags = template.tags,
content = template.content
),
pubKey = groupKey,
createdAt = template.createdAt,
tags = template.tags,
content = template.content,
sig = "d".repeat(128)
)
}
@Test
fun `the derived version is a function of the artifact and nothing else`() {
// Every input has to come off the artifact. Reading the clock here would
// still agree with itself twice in a row -- and disagree between two
// devices that applied the same artifact minutes apart, which is the
// case nobody can reproduce on demand. So the timestamp is checked
// against the artifact's rather than against a second derivation.
val artifact = signedArtifact(createdAt = 1_700_000_000)
val version = MantraArtifactVersion.initialVersionOf(artifact, chatRoomId)
assertNotNull(version)
assertEquals(1_700_000_000, version.createdAt.epochSeconds)
}
@Test
fun `two devices derive the same first version from the same artifact`() {
val artifact = signedArtifact()
val mine = MantraArtifactVersion.initialVersionOf(artifact, chatRoomId)
val theirs = MantraArtifactVersion.initialVersionOf(artifact, chatRoomId)
assertNotNull(mine)
assertEquals(mine.id, theirs?.id)
assertEquals(mine.createdAt, theirs?.createdAt)
}
@Test
fun `an artifact signed at a different moment derives a different version`() {
// The artifact's own timestamp is bound into the derived id, so two
// proposals identical but for when they were made stay two artifacts
// with two first versions rather than colliding on one row.
val first = MantraArtifactVersion.initialVersionOf(signedArtifact(createdAt = 1_700_000_000), chatRoomId)
val second = MantraArtifactVersion.initialVersionOf(signedArtifact(createdAt = 1_700_000_001), chatRoomId)
assertNotNull(first)
assertNotNull(second)
assertNotEquals(first.id, second.id)
}
@Test
fun `the derived version hangs off the artifact and carries what it declared`() {
val artifact = signedArtifact(versionLabel = "First Edition")
val version = MantraArtifactVersion.initialVersionOf(artifact, chatRoomId)
assertEquals(artifact.id, version?.artifactId)
assertEquals("First Edition", version?.versionLabel)
// Authored by whoever authored the artifact -- the group, once signed --
// and unsigned, because nobody signed this.
assertEquals(groupKey, version?.publicKey)
assertEquals("", version?.signature)
}
@Test
fun `the label is bound into the id rather than hung beside it`() {
// Two artifacts alike but for the label must not derive one version
// between them: the id has to come from the whole event, or a group
// renaming a version would leave the row it replaces in place.
val first = MantraArtifactVersion.initialVersionOf(signedArtifact(versionLabel = "1.0"), chatRoomId)
val second = MantraArtifactVersion.initialVersionOf(signedArtifact(versionLabel = "2.0"), chatRoomId)
assertNotNull(first)
assertNotNull(second)
assertNotEquals(first.id, second.id)
}
@Test
fun `an artifact that declares no version derives none`() {
// Artifacts written before the artifact carried its first version.
val declared = signedArtifact()
val silent = ArtifactEvent(
id = declared.id,
pubKey = declared.pubKey,
createdAt = declared.createdAt,
tags = declared.tags.filterNot { it.firstOrNull() == ArtifactVersionMetadataTag.TAG_NAME }
.toTypedArray(),
content = declared.content,
sig = declared.sig
)
assertNull(MantraArtifactVersion.initialVersionOf(silent, chatRoomId))
}
}
@@ -23,6 +23,7 @@ import press.mantra.compose.database.model.MantraArtifact
import press.mantra.compose.database.model.MantraArtifactVersion
import press.mantra.compose.extensions.toHex
import press.mantra.compose.nostr.nip30303.ArtifactEvent
import press.mantra.compose.nostr.nip30303.ArtifactVersionEvent
/**
* An artifact the group signed, from proposal to rows, against real FROST.
@@ -227,21 +228,70 @@ class SignedArtifactTest {
assertEquals(dialectId, artifact?.dialectId)
}
/** The second item of the batch, through the same quorum as the first. */
private fun signedInitialVersionEvent(versionLabel: String = "1.0"): ArtifactVersionEvent {
// Built from the unsigned lead, which is what `proposeSigningBatch` hands
// to `dependents` -- the artifact's id is settled at proposal, not at
// signature, which is the only reason a version can name it at all.
val lead = unsignedEventOf(proposalTemplate(versionLabel))
val item = itemOver(unsignedEventOf(ArtifactVersionEvent.initialVersionOf(lead).single()))
val signed = FrostSigningManager.signedEvent(item, groupSignature(item))
return ArtifactVersionEvent(
signed.id, signed.pubKey, signed.createdAt, signed.tags, signed.content, signed.sig
)
}
@Test
fun `the artifact arrives with the first version hanging off it`() {
// Nothing sends this row: each device derives it from the artifact it
// just applied. A chapter attaches to a version rather than to an
// artifact, so an artifact that arrives without one is inert.
val signed = signedArtifactEvent(versionLabel = "First Edition")
val artifact = MantraArtifact.fromArtifactEvent(signed, chatRoomId)
val version = MantraArtifactVersion.initialVersionOf(signed, chatRoomId)
// A chapter attaches to a version rather than to an artifact, so an
// artifact that arrives without one is inert. Both come out of one
// batch, so neither can arrive without the other.
val artifact = MantraArtifact.fromArtifactEvent(
signedArtifactEvent(versionLabel = "First Edition"), chatRoomId
)
val version = MantraArtifactVersion.fromArtifactVersionEvent(
signedInitialVersionEvent(versionLabel = "First Edition"), chatRoomId
)
assertNotNull(version)
assertEquals(artifact?.id, version.artifactId)
assertEquals("First Edition", version.versionLabel)
assertEquals(groupPubKey, version.publicKey)
// Derived, not signed: the group signed the artifact that declares it.
assertEquals("", version.signature)
}
@Test
fun `the first version carries the group's signature over its own id`() {
// The point of signing it rather than deriving it. A derived version was
// authored by the group and signed by nobody -- indistinguishable, on the
// row, from one somebody made up, and the parent of every chapter in it.
val version = MantraArtifactVersion.fromArtifactVersionEvent(
signedInitialVersionEvent(), chatRoomId
)
assertNotNull(version)
assertNotEquals("", version.signature)
assertTrue(
Nip01Crypto.verify(
signature = version.signature.hexToByteArray(),
hash = version.id.hexToByteArray(),
pubKey = version.publicKey.hexToByteArray()
),
"a first version should carry a signature the group's key made over its own id"
)
}
@Test
fun `the version is a second message, signed under its own nonce`() {
// Two items of a batch are two independent FROST instances: k signatures
// over k ids, never one signature stretched over both. A version whose
// signature verified against the artifact's id would mean a nonce had
// been reused, which is how a share is extracted rather than a cosmetic
// mix-up.
val artifact = signedArtifactEvent()
val version = signedInitialVersionEvent()
assertNotEquals(artifact.id, version.id)
assertNotEquals(artifact.sig, version.sig)
}
}
@@ -0,0 +1,160 @@
package press.mantra.compose.nostr.nip30303
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import press.mantra.compose.nostr.nip30303.tags.ArtifactVersionMetadataTag
/**
* The version an artifact starts life with, built to be signed beside it.
*
* It used to be derived on arrival from the label the artifact carries, because
* proposing it on its own would have cost a second quorum for one form. A batch
* costs one quorum, so it is signed now -- and a chapter hangs off this row,
* which is the reason to care: a version nobody signed is a parent vouched for
* by its own signed children rather than the other way round.
*
* What has to hold is that every input comes off the artifact. The id is not
* settled here -- the signing session hashes it under the group's key -- so an
* input read from anywhere else would not show up as a wrong row. It would show
* up as two devices proposing two different batches, which reads as a batch that
* simply never aggregates.
*/
class InitialArtifactVersionTest {
private val groupKey = "a".repeat(64)
private val dialectId = "b".repeat(64)
/**
* The artifact as the session will sign it: the proposer's fields re-authored
* under the group's key. This is what `proposeSigningBatch` hands to
* `dependents`, so it is what the version has to be built from.
*/
private fun leadArtifact(
versionLabel: String = "1.0",
createdAt: Long = 1_700_000_000,
): Event {
val template = ArtifactEvent.build(
name = "In Detention",
url = "example.com",
visibility = "private",
license = "cc",
dialectId = dialectId,
versionLabel = versionLabel,
createdAt = createdAt,
)
return Event(
id = EventHasher.hashId(
pubKey = groupKey,
createdAt = template.createdAt,
kind = template.kind,
tags = template.tags,
content = template.content
),
pubKey = groupKey,
createdAt = template.createdAt,
kind = template.kind,
tags = template.tags,
content = template.content,
sig = ""
)
}
/** The version item as the session would author it, so the accessors read. */
private fun versionOf(artifact: Event): ArtifactVersionEvent? =
ArtifactVersionEvent.initialVersionOf(artifact).singleOrNull()?.let { template ->
ArtifactVersionEvent(
id = EventHasher.hashId(
pubKey = groupKey,
createdAt = template.createdAt,
kind = template.kind,
tags = template.tags,
content = template.content
),
pubKey = groupKey,
createdAt = template.createdAt,
tags = template.tags,
content = template.content,
sig = ""
)
}
@Test
fun `the version names the artifact it was built from`() {
// The whole reason this takes the lead event rather than a form's fields:
// the id it carries is a hash over the group's key at the room's path, so
// there is no artifact to name until the proposal has been authored.
val artifact = leadArtifact()
assertEquals(artifact.id, versionOf(artifact)?.artifactId())
}
@Test
fun `it carries the label the artifact declared`() {
val artifact = leadArtifact(versionLabel = "First Edition")
assertEquals("First Edition", versionOf(artifact)?.content)
}
@Test
fun `it takes the artifact's own timestamp rather than the clock`() {
// So the batch reads as one act. Reading the clock here would agree with
// itself on one device and disagree between two that proposed the same
// artifact a second apart -- which is a batch nobody can aggregate, and
// the case nobody can reproduce on demand.
val artifact = leadArtifact(createdAt = 1_700_000_042)
assertEquals(1_700_000_042, versionOf(artifact)?.createdAt)
}
@Test
fun `an artifact declares exactly one first version`() {
// Item 0 is the artifact and item 1 is this, and a chapter hangs off
// whichever version it finds. Two would make that a coin toss.
assertEquals(1, ArtifactVersionEvent.initialVersionOf(leadArtifact()).size)
}
@Test
fun `two devices build the same version from the same artifact`() {
// Every signer authors the batch itself and signs the ids it arrives at.
// Devices that disagree here produce partial signatures over different
// messages, and the session stalls rather than saying why.
val artifact = leadArtifact()
assertEquals(versionOf(artifact)?.id, versionOf(artifact)?.id)
}
@Test
fun `artifacts alike but for the label do not share a version`() {
// The label is bound into the id rather than hung beside it, so renaming
// a version cannot leave the row it replaces standing.
val first = versionOf(leadArtifact(versionLabel = "1.0"))
val second = versionOf(leadArtifact(versionLabel = "2.0"))
assertNotEquals(first?.id, second?.id)
}
@Test
fun `an artifact that declares no version proposes none`() {
// Artifacts written before the artifact carried its first version. The
// batch is then one item, which is what an artifact proposal used to be.
val declared = leadArtifact()
val silent = Event(
id = declared.id,
pubKey = declared.pubKey,
createdAt = declared.createdAt,
kind = declared.kind,
tags = declared.tags.filterNot { it.firstOrNull() == ArtifactVersionMetadataTag.TAG_NAME }
.toTypedArray(),
content = declared.content,
sig = declared.sig
)
assertTrue(ArtifactVersionEvent.initialVersionOf(silent).isEmpty())
assertNull(versionOf(silent))
}
}