mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
docs(mls): record the engine boundary; last hardcoded profile bits
Three leftovers from the extraction, plus the documentation it needs to
survive.
`KeyPackageRotationManager` still built its leaf capabilities from bare hex
(`0x000A`, `0xF2EE`, `0x000A`) with the two meanings of `0x000A` -- last_resort
as an extension, self_remove as a proposal -- distinguishable only by which
list they were in. That set is now `MarmotCapabilities.mipKeyPackageLeaf()`,
beside the other two profiles, with the ordering marked load-bearing: those
bytes go into published KeyPackages and define KeyPackageBundleStore's v4
snapshot format.
`MarmotManager` and `MarmotConvergenceEngine` still spelled
`exporterSecret("marmot", "group-event", 32)` literally at three call sites, so
the binding's key derivation was stated in four places. They read it off
`MarmotGroupPolicy.commitExporter` now, which is where the engine reads it too.
`CurrentProfileWelcomeTest` and `CommitPreservesLeafIdentityTest` sat in the
`mls/` test tree with ten and three Marmot imports between them; they test
Marmot's current profile, so they move to `marmot/appComponents/`.
The README states the invariant as a command you can run, and scopes it
honestly: shipped code only. Two tests under `mls/components/` do decode real
Marmot components, because an interop test is worth more against payloads that
actually exist -- a fixture is data, an import in the engine is a dependency.
It also writes down the three things a second binding has to know, the one
that will bite (a policy is behaviour, not state, so a restore that forgets it
silently drops the rules), and why the default is permissive rather than
closed.
The plan's Stage 1 is marked landed with what it cost and what Stage 3 still
owes: MlsGroupManager names `nostrGroupId` 147 times, so cordn needs its own
manager over the same MlsGroup rather than reusing that one. §5.1's coupling
measurements are marked superseded rather than deleted -- they are what the
stage was scoped against.
Verified: :quartz:jvmTest 5082, :commons:jvmTest 2202, both green;
:quic, :cli, :marmotBench and :amethyst all compile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
@@ -207,8 +207,8 @@ import com.vitorpamplona.quartz.experimental.profileGallery.hash
|
|||||||
import com.vitorpamplona.quartz.experimental.profileGallery.image
|
import com.vitorpamplona.quartz.experimental.profileGallery.image
|
||||||
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.cli.stores
|
|||||||
import com.vitorpamplona.amethyst.cli.SecureFileIO
|
import com.vitorpamplona.amethyst.cli.SecureFileIO
|
||||||
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
|
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
|
||||||
import com.vitorpamplona.quartz.marmot.MarmotIngestDedupStore
|
import com.vitorpamplona.quartz.marmot.MarmotIngestDedupStore
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|||||||
+8
-5
@@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotGroupSnapshot
|
|||||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotMessageEdit
|
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotMessageEdit
|
||||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemEvent
|
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemEvent
|
||||||
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemRowDiff
|
import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemRowDiff
|
||||||
|
import com.vitorpamplona.quartz.marmot.groups.MarmotGroupPolicy
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupManager
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupManager
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
@@ -90,14 +91,14 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
|||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||||
import kotlin.io.encoding.Base64
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central coordinator for Marmot MLS group messaging.
|
* Central coordinator for Marmot MLS group messaging.
|
||||||
@@ -327,9 +328,11 @@ class MarmotManager(
|
|||||||
): ByteArray? =
|
): ByteArray? =
|
||||||
try {
|
try {
|
||||||
val preCommitKey =
|
val preCommitKey =
|
||||||
MlsGroup
|
MarmotGroupPolicy.commitExporter.let { exporter ->
|
||||||
.restore(obligation.priorState)
|
MlsGroup
|
||||||
.exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
.restore(obligation.priorState, MarmotGroupPolicy)
|
||||||
|
.exporterSecret(exporter.label, exporter.context, exporter.length)
|
||||||
|
}
|
||||||
GroupEventEncryption.decrypt(event.content, preCommitKey)
|
GroupEventEncryption.decrypt(event.content, preCommitKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(
|
Log.w(
|
||||||
|
|||||||
+2
-2
@@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.PreviewStat
|
|||||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicException
|
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicException
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicStream
|
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicStream
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|||||||
+2
-2
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
|
|||||||
+2
-2
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
|
|||||||
+2
-2
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
|
|||||||
+1
-1
@@ -20,8 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||||
import com.vitorpamplona.quartz.marmot.protocolCore.GroupLifecycleState
|
import com.vitorpamplona.quartz.marmot.protocolCore.GroupLifecycleState
|
||||||
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
import com.vitorpamplona.quartz.marmot.protocolCore.MarmotPublishObligationStore
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
|||||||
+1
-1
@@ -20,9 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
|
||||||
// In-memory stand-ins for the durable stores a MarmotManager needs.
|
// In-memory stand-ins for the durable stores a MarmotManager needs.
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
package com.vitorpamplona.marmotbench
|
package com.vitorpamplona.marmotbench
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.marmot.MarmotPublisher
|
import com.vitorpamplona.amethyst.commons.marmot.MarmotPublisher
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
import com.vitorpamplona.quartz.marmot.groups.MarmotMessageStore
|
||||||
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
import com.vitorpamplona.quartz.marmot.groups.MlsGroupStateStore
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageBundleStore
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
|
||||||
// In-memory stores, matching the commons test doubles byte for byte.
|
// In-memory stores, matching the commons test doubles byte for byte.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Cordn interop: extract the MLS core, then add a second binding
|
# Cordn interop: extract the MLS core, then add a second binding
|
||||||
|
|
||||||
Status: Stage 2 landed. `:contextvm` implements the core spec plus all 12 CEPs on the client
|
Status: Stages 1 and 2 landed. The RFC 9420 engine is `quartz/…/mls/` and imports nothing from
|
||||||
side, with the Tier C fixture server, at 172 tests. Stages 0 (cordn-side vectors), 1 (extract the
|
`marmot/` — a binding supplies its rules through `MlsGroupPolicy`. `:contextvm` implements the
|
||||||
MLS engine) and 3-4 (the cordn binding and app integration) are open. Stage 3 is the one gated on
|
core spec plus all 12 CEPs on the client side, with the Tier C fixture server, at 172 tests.
|
||||||
the §4.1 upstream decision.
|
Stages 0 (cordn-side vectors) and 3-4 (the cordn binding and app integration) are open; Stage 3
|
||||||
|
is the one gated on the §4.1 upstream decision.
|
||||||
|
|
||||||
Correction to an earlier gate in this plan: §4.1 does **not** block Stage 2. ContextVM is
|
Correction to an earlier gate in this plan: §4.1 does **not** block Stage 2. ContextVM is
|
||||||
credential-agnostic and has no MLS dependency at all, so the transport was safe to build first;
|
credential-agnostic and has no MLS dependency at all, so the transport was safe to build first;
|
||||||
@@ -201,7 +202,11 @@ wire format. There is nothing to implement against. Do not attempt it.
|
|||||||
|
|
||||||
### 5.1 The engine, and how Marmot-clean it is
|
### 5.1 The engine, and how Marmot-clean it is
|
||||||
|
|
||||||
`quartz/…/marmot/mls/` is **11,964 LOC**. Measured coupling to the Marmot layer:
|
**Superseded by Stage 1, which landed.** The measurements below are what the engine looked like
|
||||||
|
before the extraction; they are kept because they are what the stage was scoped against. The
|
||||||
|
engine is now `quartz/…/mls/` with zero `marmot/` imports.
|
||||||
|
|
||||||
|
`quartz/…/marmot/mls/` was **11,964 LOC**. Measured coupling to the Marmot layer:
|
||||||
|
|
||||||
- **3 files**, **10 imports** total:
|
- **3 files**, **10 imports** total:
|
||||||
- `group/MlsGroup.kt` (7): `MarmotGroupData`, `MarmotGroupState`, `AdminPolicyV1`,
|
- `group/MlsGroup.kt` (7): `MarmotGroupData`, `MarmotGroupState`, `AdminPolicyV1`,
|
||||||
@@ -637,29 +642,57 @@ No production code. Two deliverables.
|
|||||||
|
|
||||||
**Gate:** do not start Stage 2 until §4.1 has an answer. Stage 1 is safe to do regardless.
|
**Gate:** do not start Stage 2 until §4.1 has an answer. Stage 1 is safe to do regardless.
|
||||||
|
|
||||||
### Stage 1 — Extract a binding-agnostic RFC 9420 engine
|
### Stage 1 — Extract a binding-agnostic RFC 9420 engine — LANDED
|
||||||
|
|
||||||
Worth doing whether or not cordn ever ships. Today the engine is one protocol's private detail;
|
Done in four commits. The engine is `quartz/…/mls/` and imports nothing from `quartz/…/marmot/`.
|
||||||
this makes it a library.
|
|
||||||
|
|
||||||
- Move `quartz/…/marmot/mls/` → `quartz/…/mls/`. Packages change; `marmot/` keeps everything
|
| What | Where it went |
|
||||||
else.
|
| ---- | ------------- |
|
||||||
- Parameterize what §5.1 lists as hardcoded: group id, credential identity bytes,
|
| The engine (28 files, 11,964 LOC) | `quartz/…/marmot/mls/` → `quartz/…/mls/` |
|
||||||
`required_capabilities`, leaf `capabilities`, and the exporter label/context all become
|
| `MlsGroupManager`, `MlsGroupStateStore`, `MarmotMessageStore` | → `marmot/groups/` (all keyed on `nostrGroupId`) |
|
||||||
constructor or call-site inputs. `exporterSecret(label, context, length)` already exists —
|
| MIP-03 authorization, depletion guard, join role check, self-remove gate | → `marmot/groups/MarmotGroupPolicy` |
|
||||||
stop calling it with a literal `"marmot"` from inside the engine.
|
| Leaf + `required_capabilities` profiles | → `marmot/groups/MarmotCapabilities` |
|
||||||
- Push the Marmot-specific reads out to `marmot/`: `currentMarmotData()`, `currentGroupState()`,
|
| `currentMarmotData/GroupState/NostrGroupId`, `agentTextStreamSecret` | → `marmot/groups/MarmotGroupViews` (extension functions) |
|
||||||
`currentNostrGroupId()`, the `AdminPolicyV1`/`GroupLifecycleV1` hooks and the agent-text-stream
|
| `last_resort_key_package` (0x0004) | → `mls/components/ComponentsList` — it is the extensions draft's, not Marmot's |
|
||||||
helpers. Where the engine needs a policy decision, it takes an interface; `marmot/` supplies the
|
|
||||||
Marmot implementation.
|
|
||||||
- Move `group/MarmotMessageStore.kt` out (it is named for Marmot and keyed on
|
|
||||||
`nostrGroupId` — it is a binding concern).
|
|
||||||
- Behaviour must not change. The existing MLS + Marmot test suites are the contract; they pass
|
|
||||||
unmodified except for import lines.
|
|
||||||
|
|
||||||
Risk: `MlsGroup.kt` is 4,505 lines and carries the convergence/lifecycle logic. Keep this stage
|
**The seam is `MlsGroupPolicy`**: three hooks the engine calls where RFC 9420 defers to the
|
||||||
strictly mechanical — extraction and parameterization, no logic edits — so the diff stays
|
application (`authorizeCommit`, `authorizeSelfRemove`, `validateJoin`) and four values it reads
|
||||||
reviewable and the test suite is a real check.
|
(leaf capabilities, `required_capabilities`, extra known extension types, the commit exporter
|
||||||
|
label). One argument selects a whole profile — `MlsGroup.create(id, policy = MarmotGroupPolicy)`
|
||||||
|
brings the rules, the capabilities and `MLS-Exporter("marmot", "group-event", 32)` together — so
|
||||||
|
adopting it cost one added argument per call site rather than five.
|
||||||
|
|
||||||
|
Policies receive a read-only `GroupView`, not the `MlsGroup`: a policy holding the group could
|
||||||
|
commit or rotate keys from inside the check meant to gate those things.
|
||||||
|
|
||||||
|
**The default is permissive**, which is a trade worth naming. Closed would make the engine
|
||||||
|
unusable without a policy and would push callers into writing an allow-everything one anyway.
|
||||||
|
The cost is that a group restored without its policy silently drops the binding's rules — a
|
||||||
|
policy is behaviour, not state, so it is deliberately not in `MlsGroupState`. All ten production
|
||||||
|
construction sites are in `marmot/` and all ten name it.
|
||||||
|
|
||||||
|
Two findings from doing it:
|
||||||
|
|
||||||
|
1. **`:quic` was already a second consumer, reaching through the wrong package.**
|
||||||
|
`quic/tls/TlsClient.kt` imported `quartz.marmot.mls.crypto.X25519` for its TLS 1.3 handshake —
|
||||||
|
neither Marmot nor MLS. That is the argument for this stage independent of cordn.
|
||||||
|
2. **The test suite found every site that had been relying on Marmot defaults.** The first run
|
||||||
|
after `MlsGroup.create` stopped defaulting to Marmot's profile failed 13 tests, each one a
|
||||||
|
Marmot test that had been getting a Marmot-shaped group for free. The other ~180 construction
|
||||||
|
sites kept passing on the permissive default — they are engine tests, and they now prove the
|
||||||
|
engine runs without Marmot at all.
|
||||||
|
|
||||||
|
8 tests were added for the seam itself (`MlsGroupPolicySeamTest`, `MarmotPolicySeamTest`),
|
||||||
|
including the half no existing test covered: the same group at the same state accepts the same
|
||||||
|
commit once the policy is gone. Verified by mutation — ignoring the policy in `commit()` and
|
||||||
|
re-hardcoding the exporter label each kill exactly their guarding tests.
|
||||||
|
|
||||||
|
Suite: `:quartz:jvmTest` 5074 → 5082, `:commons:jvmTest` 2202, both green.
|
||||||
|
|
||||||
|
What Stage 3 still owes: `MlsGroupManager` is keyed on `nostrGroupId` 147 times, so cordn cannot
|
||||||
|
reuse it and needs its own manager over the same `MlsGroup`. Class names were left alone in the
|
||||||
|
move — `MlsGroupManager` under `marmot/groups/` reads correctly and renaming four classes would
|
||||||
|
have churned 22 files across five modules for clarity the package path already gives.
|
||||||
|
|
||||||
### Stage 2 — `:contextvm` module (clean-room) — LANDED
|
### Stage 2 — `:contextvm` module (clean-room) — LANDED
|
||||||
|
|
||||||
|
|||||||
+19
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
|||||||
import com.vitorpamplona.quartz.mls.codec.TlsWriter
|
import com.vitorpamplona.quartz.mls.codec.TlsWriter
|
||||||
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
||||||
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||||
|
import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage
|
||||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
||||||
import com.vitorpamplona.quartz.mls.tree.Credential
|
import com.vitorpamplona.quartz.mls.tree.Credential
|
||||||
import com.vitorpamplona.quartz.mls.tree.Extension
|
import com.vitorpamplona.quartz.mls.tree.Extension
|
||||||
@@ -59,6 +60,24 @@ object MarmotCapabilities {
|
|||||||
proposals = listOf(MlsGroup.SELF_REMOVE_PROPOSAL_TYPE),
|
proposals = listOf(MlsGroup.SELF_REMOVE_PROPOSAL_TYPE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MIP-era leaf set as a published KeyPackage advertises it.
|
||||||
|
*
|
||||||
|
* Same as [mipLeaf] plus `0x000A` as an EXTENSION, which OpenMLS validation
|
||||||
|
* requires on a last-resort KeyPackage. Note `0x000A` appears in both lists
|
||||||
|
* meaning different things: as an extension it is `last_resort`, as a
|
||||||
|
* proposal it is `self_remove`.
|
||||||
|
*
|
||||||
|
* The order is load-bearing and must not be tidied: these bytes go into
|
||||||
|
* published KeyPackages, and `KeyPackageBundleStore`'s v4 snapshot format
|
||||||
|
* is defined by them.
|
||||||
|
*/
|
||||||
|
fun mipKeyPackageLeaf(): Capabilities =
|
||||||
|
Capabilities(
|
||||||
|
extensions = listOf(MlsKeyPackage.LAST_RESORT_EXTENSION_TYPE, MARMOT_GROUP_DATA_EXTENSION_TYPE),
|
||||||
|
proposals = listOf(MlsGroup.SELF_REMOVE_PROPOSAL_TYPE),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an MLS `required_capabilities` extension that marks Marmot's
|
* Build an MLS `required_capabilities` extension that marks Marmot's
|
||||||
* mandatory interop set as required for all members (RFC 9420 §7.2):
|
* mandatory interop set as required for all members (RFC 9420 §7.2):
|
||||||
|
|||||||
+2
-13
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
package com.vitorpamplona.quartz.marmot.mip00KeyPackages
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||||
|
import com.vitorpamplona.quartz.marmot.groups.MarmotCapabilities
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.SNAPSHOT_VERSION
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.SNAPSHOT_VERSION
|
||||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MlsCiphersuite
|
import com.vitorpamplona.quartz.marmot.mip01Groups.MlsCiphersuite
|
||||||
import com.vitorpamplona.quartz.mls.codec.TlsReader
|
import com.vitorpamplona.quartz.mls.codec.TlsReader
|
||||||
@@ -30,7 +31,6 @@ import com.vitorpamplona.quartz.mls.crypto.MlsCryptoProvider
|
|||||||
import com.vitorpamplona.quartz.mls.crypto.X25519
|
import com.vitorpamplona.quartz.mls.crypto.X25519
|
||||||
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
||||||
import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage
|
import com.vitorpamplona.quartz.mls.messages.MlsKeyPackage
|
||||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
|
||||||
import com.vitorpamplona.quartz.mls.tree.Credential
|
import com.vitorpamplona.quartz.mls.tree.Credential
|
||||||
import com.vitorpamplona.quartz.mls.tree.Extension
|
import com.vitorpamplona.quartz.mls.tree.Extension
|
||||||
import com.vitorpamplona.quartz.mls.tree.LeafNode
|
import com.vitorpamplona.quartz.mls.tree.LeafNode
|
||||||
@@ -654,18 +654,7 @@ class KeyPackageRotationManager(
|
|||||||
encryptionKey = encryptionKey,
|
encryptionKey = encryptionKey,
|
||||||
signatureKey = signatureKey,
|
signatureKey = signatureKey,
|
||||||
credential = Credential.Basic(identity),
|
credential = Credential.Basic(identity),
|
||||||
capabilities =
|
capabilities = MarmotCapabilities.mipKeyPackageLeaf(),
|
||||||
Capabilities(
|
|
||||||
extensions =
|
|
||||||
listOf(
|
|
||||||
0x000A, // LastResort (required by OpenMLS validation)
|
|
||||||
0xF2EE, // NostrGroupData (required by group's RequiredCapabilities)
|
|
||||||
),
|
|
||||||
proposals =
|
|
||||||
listOf(
|
|
||||||
0x000A, // SelfRemove (required by group's RequiredCapabilities)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leafNodeSource = LeafNodeSource.KEY_PACKAGE,
|
leafNodeSource = LeafNodeSource.KEY_PACKAGE,
|
||||||
lifetime = Lifetime(notBefore = now, notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS),
|
lifetime = Lifetime(notBefore = now, notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS),
|
||||||
extensions = emptyList(),
|
extensions = emptyList(),
|
||||||
|
|||||||
+3
-1
@@ -423,7 +423,9 @@ class MarmotConvergenceEngine(
|
|||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
contexts[groupId]?.candidateStates?.values?.mapNotNull { state ->
|
contexts[groupId]?.candidateStates?.values?.mapNotNull { state ->
|
||||||
try {
|
try {
|
||||||
MlsGroup.restore(state, MarmotGroupPolicy).exporterSecret("marmot", "group-event".encodeToByteArray(), 32)
|
MarmotGroupPolicy.commitExporter.let {
|
||||||
|
MlsGroup.restore(state, MarmotGroupPolicy).exporterSecret(it.label, it.context, it.length)
|
||||||
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# `mls/` — the RFC 9420 engine
|
||||||
|
|
||||||
|
A binding-agnostic MLS (RFC 9420) implementation: TLS presentation-language
|
||||||
|
codec, HPKE, X25519/Ed25519, the ratchet tree, the key schedule, the secret
|
||||||
|
tree, message framing, proposals, commits, Welcome, and the `app_data_dictionary`
|
||||||
|
component carrier from `draft-ietf-mls-extensions-10`.
|
||||||
|
|
||||||
|
**It knows nothing about Marmot, Nostr, or cordn, and must stay that way.** The
|
||||||
|
invariant, over the shipped code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn 'import com.vitorpamplona.quartz.marmot' \
|
||||||
|
--include='*.kt' quartz/src/{commonMain,jvmAndroid,appleMain,linuxMain}/kotlin/com/vitorpamplona/quartz/mls/
|
||||||
|
```
|
||||||
|
|
||||||
|
That must print nothing. Tests are deliberately outside it: a couple under
|
||||||
|
`mls/components/` decode real Marmot components as fixtures, because the point
|
||||||
|
of an interop test is to exercise the engine against payloads that actually
|
||||||
|
exist. A fixture is data; an import in the engine is a dependency.
|
||||||
|
|
||||||
|
It lived under `marmot/` until Stage 1 of
|
||||||
|
`quartz/plans/2026-09-17-cordn-interop.md`, and `:quic` was already importing
|
||||||
|
`crypto/X25519` from there for its TLS 1.3 handshake — a use that is neither
|
||||||
|
Marmot nor MLS, and the reason the extraction was overdue.
|
||||||
|
|
||||||
|
## Using it
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val group = MlsGroup.create(identity) // a plain RFC 9420 group
|
||||||
|
val group = MlsGroup.create(identity, policy = MarmotGroupPolicy) // Marmot's profile
|
||||||
|
```
|
||||||
|
|
||||||
|
The default group requires nothing beyond RFC 9420: no `required_capabilities`,
|
||||||
|
no leaf capabilities (§7.2 forbids advertising DEFAULT types), no commit
|
||||||
|
exporter, and no limit on who may commit what.
|
||||||
|
|
||||||
|
## The `MlsGroupPolicy` seam
|
||||||
|
|
||||||
|
RFC 9420 says who may *send* a proposal and never who may *commit* one. Real
|
||||||
|
deployments need more — Marmot's MIP-03 names admin accounts and allows everyone
|
||||||
|
else only a self-Update or a SelfRemove — so the engine asks a policy wherever
|
||||||
|
the spec defers to the application:
|
||||||
|
|
||||||
|
| Hook | Called from | Marmot uses it for |
|
||||||
|
| ---- | ----------- | ------------------ |
|
||||||
|
| `authorizeCommit` | `commit()` and both inbound commit paths | MIP-03 proposal-set rules + admin depletion |
|
||||||
|
| `authorizeSelfRemove` | `proposeSelfRemove`, `buildSelfRemoveProposalMessage` | admin must self-demote first |
|
||||||
|
| `validateJoin` | `processWelcome` | `required_member_roles` on the agent-text-stream component |
|
||||||
|
| `defaultLeafCapabilities` | every leaf the engine builds | `0xF2EE` + `self_remove` |
|
||||||
|
| `defaultRequiredCapabilities` | `create()`, epoch 0 | the MIP-era interop set |
|
||||||
|
| `knownExtensionTypes` | GroupContextExtensions validation | `0xF2EE` |
|
||||||
|
| `commitExporter` | `CommitResult.preCommitExporterSecret` | `MLS-Exporter("marmot", "group-event", 32)` |
|
||||||
|
|
||||||
|
Three things to know before you add a binding:
|
||||||
|
|
||||||
|
1. **One argument selects a whole profile.** `policy` supplies the rules *and*
|
||||||
|
the capability defaults *and* the exporter binding, so `capabilities` and
|
||||||
|
`requiredCapabilities` default from it. Adopting a profile is one argument,
|
||||||
|
not five.
|
||||||
|
|
||||||
|
2. **A policy is behaviour, not state.** It is deliberately absent from
|
||||||
|
`MlsGroupState`, so whatever restores a group must pass the same policy it
|
||||||
|
was created with. A restore that forgets it gets a group that silently skips
|
||||||
|
the binding's rules. In Marmot only `MlsGroupManager` restores a group in
|
||||||
|
order to commit with it, which is what keeps that narrow.
|
||||||
|
|
||||||
|
3. **A policy gets a `GroupView`, not the `MlsGroup`.** A policy holding the
|
||||||
|
group could commit or rotate keys from inside the check meant to gate exactly
|
||||||
|
that.
|
||||||
|
|
||||||
|
The default is permissive rather than closed. Closed would make the engine
|
||||||
|
unusable without a policy and would push callers into writing an
|
||||||
|
allow-everything one anyway; open puts each restriction in the binding that
|
||||||
|
documents it. The cost is item 2 above.
|
||||||
|
|
||||||
|
## Where the bindings live
|
||||||
|
|
||||||
|
- `quartz/…/marmot/groups/` — `MarmotGroupPolicy`, `MarmotCapabilities`,
|
||||||
|
`MarmotGroupViews` (Marmot's reads of a GroupContext, as extension functions),
|
||||||
|
plus `MlsGroupManager` and the two stores, all keyed on `nostrGroupId`.
|
||||||
|
- `quartz/…/marmot/mipXX…/` — the Nostr event layer.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`quartz/src/commonTest/…/mls/` runs everywhere; `quartz/src/jvmAndroidTest/…/mls/`
|
||||||
|
holds the tests needing real secp256k1/JNI. `mls/interop/` replays the RFC 9420
|
||||||
|
test vectors. `group/MlsGroupPolicySeamTest` pins the seam itself with a
|
||||||
|
recording policy; `marmot/groups/MarmotPolicySeamTest` pins that Marmot's rules
|
||||||
|
travel with `MarmotGroupPolicy` and not with the engine.
|
||||||
+2
-1
@@ -18,12 +18,13 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.mls.group
|
package com.vitorpamplona.quartz.marmot.appComponents
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.AppComponentIds
|
import com.vitorpamplona.quartz.marmot.appComponents.AppComponentIds
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
||||||
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
import com.vitorpamplona.quartz.mls.components.AppDataDictionary
|
||||||
|
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
+2
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.mls.group
|
package com.vitorpamplona.quartz.marmot.appComponents
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
import com.vitorpamplona.quartz.marmot.appComponents.CurrentProfileGroupFactory
|
||||||
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
import com.vitorpamplona.quartz.marmot.appComponents.GroupProfileV1
|
||||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.marmot.groups.currentMarmotData
|
|||||||
import com.vitorpamplona.quartz.marmot.groups.currentNostrGroupId
|
import com.vitorpamplona.quartz.marmot.groups.currentNostrGroupId
|
||||||
import com.vitorpamplona.quartz.mls.crypto.Ed25519
|
import com.vitorpamplona.quartz.mls.crypto.Ed25519
|
||||||
import com.vitorpamplona.quartz.mls.crypto.Ed25519KeyPair
|
import com.vitorpamplona.quartz.mls.crypto.Ed25519KeyPair
|
||||||
|
import com.vitorpamplona.quartz.mls.group.MlsGroup
|
||||||
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
import com.vitorpamplona.quartz.mls.messages.KeyPackageBundle
|
||||||
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
import com.vitorpamplona.quartz.mls.tree.Capabilities
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||||
Reference in New Issue
Block a user