From a7dbc350455002bc7e6a6d57cabfeca794fde399 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Mon, 19 Jan 2026 22:15:48 -0800 Subject: [PATCH 01/14] decrypt follow welcome --- .../DistributedMLS/Group/Decrypt/DecryptOutput.swift | 2 +- Sources/DistributedMLS/Group/DiGroup.swift | 12 +++++++++--- Sources/DistributedMLS/Group/ReceiveChannel.swift | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index 01fe5b1..be84d4b 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -17,7 +17,7 @@ extension DiMLS { } - public struct AppPlaintext: Sendable { + public struct AppPlaintext: Sendable, Equatable { public let application: Data public let authenticating: Data diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 08aabd8..e9d11cf 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -38,7 +38,7 @@ extension DiMLS { func prepareCommit() throws -> DiMLS.CommitInput //different interface as it is initially handled by the init key //corresponding to a keyPackage - func received(welcome: WelcomeOutput) throws + func received(welcome: WelcomeOutput) throws -> AppPlaintext? //if we know we can process directly with the symmetric ratchet func received(privateMessage: Data) throws -> AppPlaintext func received(ciphertext: Data) throws -> DecryptOutput @@ -219,7 +219,7 @@ extension DiMLS.DiGroup { ) } - public func received(welcome: WelcomeOutput) throws { + public func received(welcome: WelcomeOutput) throws -> DiMLS.AppPlaintext? { guard welcome.diGroupId == diGroupId else { throw DiMLSError.mismatchedGroupId } @@ -234,8 +234,14 @@ extension DiMLS.DiGroup { throw DiMLSError.duplicateMember } receivers[senderReferenceId] = try .create(welcome: welcome) - } + guard let appMessage = welcome.appPrivateMessage else { + return nil + } + return try receivers[senderReferenceId].tryUnwrap + .decrypt(messageData: appMessage) + + } } //we have a generic (Credential) and a non-generic and could simplify them diff --git a/Sources/DistributedMLS/Group/ReceiveChannel.swift b/Sources/DistributedMLS/Group/ReceiveChannel.swift index 575d20a..8d76017 100644 --- a/Sources/DistributedMLS/Group/ReceiveChannel.swift +++ b/Sources/DistributedMLS/Group/ReceiveChannel.swift @@ -10,4 +10,5 @@ import Foundation public protocol ReceiveChannel { associatedtype WelcomeOutput: WelcomeOutputInterface static func create(welcome: WelcomeOutput) throws -> Self + func decrypt(messageData: Data) throws -> DiMLS.AppPlaintext } From 49ab81657e7c31a660c7b69a63b7c9f1f95ffa11 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 20 Jan 2026 01:15:05 -0800 Subject: [PATCH 02/14] tweaks for intial welcome processing --- Sources/DistributedMLS/Group/DiGroup.swift | 5 ++-- .../DistributedMLS/Group/PendingState.swift | 28 ++++++++++--------- .../DistributedMLS/Group/SendChannel.swift | 4 +-- .../Interfaces/WelcomeOutputInterface.swift | 2 ++ .../TotalGroup/TotalGroup.swift | 16 ++++++++--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index e9d11cf..fbe01fc 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -17,6 +17,7 @@ extension DiMLS { associatedtype Sender: SendChannel where Sender.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface + where WelcomeOutput.Credential == Credential //Archivable var archive: Archive { get throws } @@ -38,7 +39,6 @@ extension DiMLS { func prepareCommit() throws -> DiMLS.CommitInput //different interface as it is initially handled by the init key //corresponding to a keyPackage - func received(welcome: WelcomeOutput) throws -> AppPlaintext? //if we know we can process directly with the symmetric ratchet func received(privateMessage: Data) throws -> AppPlaintext func received(ciphertext: Data) throws -> DecryptOutput @@ -156,7 +156,7 @@ extension DiMLS.DiGroup { plaintext: Data, authenticating: Data, staplingCommit: Bool - ) throws -> [(Credential, DiMLS.EncryptOutput)] { + ) throws -> [Credential: DiMLS.EncryptOutput] { let sender = try lazySender.readyChannel //commit if necessary @@ -234,6 +234,7 @@ extension DiMLS.DiGroup { throw DiMLSError.duplicateMember } receivers[senderReferenceId] = try .create(welcome: welcome) + try totalGroup.welcomed(member: try welcome.membershipEpoch) guard let appMessage = welcome.appPrivateMessage else { return nil diff --git a/Sources/DistributedMLS/Group/PendingState.swift b/Sources/DistributedMLS/Group/PendingState.swift index ee876aa..56342f9 100644 --- a/Sources/DistributedMLS/Group/PendingState.swift +++ b/Sources/DistributedMLS/Group/PendingState.swift @@ -10,21 +10,21 @@ import os extension DiMLS { public final class PendingState { var applicationRequestsNewKeyDistribution: Bool - public var pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations] + public var pendingLocalOps: Set> //this includes incorporating PCS updates var pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] public static func create() -> Self { .init( applicationRequestsNewKeyDistribution: false, - pendingLocalOps: [:], + pendingLocalOps: [], pendingFollowOps: [:] ) } init( applicationRequestsNewKeyDistribution: Bool, - pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations], + pendingLocalOps: Set>, pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] ) { self.applicationRequestsNewKeyDistribution = applicationRequestsNewKeyDistribution @@ -35,7 +35,7 @@ extension DiMLS { //MARK: Archive public struct Archive: Sendable, Codable { let applicationRequestsNewKeyDistribution: Bool - let pendingLocalOps: [DiMLS.ReferenceID: DiMLSOperations.Archive] + let pendingLocalOps: [DiMLSOperations.Archive] //this includes incorporating PCS updates let pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations.Archive] } @@ -43,8 +43,10 @@ extension DiMLS { public init(archive: Archive) throws { self.applicationRequestsNewKeyDistribution = archive.applicationRequestsNewKeyDistribution - self.pendingLocalOps = try archive.pendingLocalOps - .mapValues { try .init(archive: $0) } + self.pendingLocalOps = try .init( + archive.pendingLocalOps.map { + try .init(archive: $0) + }) self.pendingFollowOps = try archive.pendingFollowOps .mapValues { try .init(archive: $0) } } @@ -53,7 +55,7 @@ extension DiMLS { get throws { .init( applicationRequestsNewKeyDistribution: applicationRequestsNewKeyDistribution, - pendingLocalOps: try pendingLocalOps.mapValues { try $0.archive }, + pendingLocalOps: try pendingLocalOps.map { try $0.archive }, pendingFollowOps: try pendingFollowOps.mapValues { try $0.archive }, ) } @@ -65,13 +67,13 @@ extension DiMLS { } public func stageAdd(member: DiMLS.CredentialedKeyPackage) throws { - //already staged conflicting op? we can just replace for now - if let pendingOp = pendingLocalOps[member.credential.referenceId] { - Logger(subsystem: "SenderGroupDiMLS", category: "validAdd") - .notice("substituting a staged \(pendingOp.description)") - } + pendingLocalOps.insert(.add(member)) + } - pendingLocalOps[member.credential.referenceId] = .add(member) + func committed(input: CommitInput) { + for localOp in input.localOps { + pendingLocalOps.remove(localOp) + } } } } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 3b7975c..6fde744 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -43,9 +43,7 @@ public protocol SendChannel { ) //packaging the encrypted app message with metadata can all be done //in the send channel - func encrypt(plaintext: Data, authenticating: Data) throws -> [( - Credential, DiMLS.EncryptOutput - )] + func encrypt(plaintext: Data, authenticating: Data) throws -> [Credential: DiMLS.EncryptOutput] } diff --git a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift index 3a67f64..04d00e2 100644 --- a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift +++ b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift @@ -8,8 +8,10 @@ import Foundation public protocol WelcomeOutputInterface: Sendable { + associatedtype Credential: DiMLSCredential var diGroupId: Data { get } var senderReferenceId: DiMLS.ReferenceID { get throws } var keyedDependency: DiMLS.KeyedDependency? { get } var appPrivateMessage: Data? { get } + var membershipEpoch: DiMLS.TotalGroup.Membership.Epoch { get throws } } diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 93cbb52..241770d 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -71,6 +71,14 @@ extension DiMLS { try members[member].tryUnwrap .readyToWelcome(member: member) } + + func welcomed(member: Membership.Epoch) throws { + let referenceId = member.credential.referenceId + guard case .invited = members[referenceId] else { + throw DiMLSError.disallowed + } + members[referenceId] = .claimed([member]) + } } } @@ -105,10 +113,10 @@ extension DiMLS.TotalGroup { } public struct Epoch: Archivable { - let epoch: UInt64 + let epoch: DiMLS.EpochID public let credential: Credential - init(epoch: UInt64, credential: Credential) { + public init(epoch: DiMLS.EpochID, credential: Credential) { self.epoch = epoch self.credential = credential } @@ -121,10 +129,10 @@ extension DiMLS.TotalGroup { } public struct Archive: Codable, Sendable { - let epoch: UInt64 + let epoch: DiMLS.EpochID let credential: Data - public init(epoch: UInt64, credential: Data) { + public init(epoch: DiMLS.EpochID, credential: Data) { self.epoch = epoch self.credential = credential } From 509beab1f6cd6dcafc7a6f31e3c2ee3dba8d4280 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 20 Jan 2026 17:14:02 -0800 Subject: [PATCH 03/14] enable commit processing --- Sources/DistributedMLS/Client.swift | 2 +- .../Group/Decrypt/DecryptOutput.swift | 17 ++++++++++++++--- Sources/DistributedMLS/Group/DiGroup.swift | 18 ++++++++++-------- ...mitEffect.swift => LocalCommitEffect.swift} | 4 ++-- .../DistributedMLS/Group/ReceiveChannel.swift | 1 + Sources/DistributedMLS/Helpers/Error.swift | 2 ++ 6 files changed, 30 insertions(+), 14 deletions(-) rename Sources/DistributedMLS/Group/{CommitEffect.swift => LocalCommitEffect.swift} (83%) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index 4ce8cca..50ab870 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -11,7 +11,7 @@ extension DiMLS { public protocol Client: Actor, Archivable { associatedtype Credential: DiMLSCredential associatedtype Group: DiGroup - where Group.WelcomeOutput == WelcomeOutput, Group.Credential == Credential + where Group.Receiver.WelcomeOutput == WelcomeOutput, Group.Credential == Credential associatedtype WelcomeOutput: WelcomeOutputInterface static func create(credential: Credential) throws -> Self diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index be84d4b..f7b82b3 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -9,12 +9,23 @@ import Foundation extension DiMLS { public struct DecryptOutput: Sendable { - let appPlaintext: AppPlaintext - let controlMessage: ControlMessage? + public let appPlaintext: AppPlaintext + public let commitResult: CommitResult? + + public init( + appPlaintext: AppPlaintext, + commitResult: CommitResult? + ) { + self.appPlaintext = appPlaintext + self.commitResult = commitResult + } } - public struct ControlMessage: Sendable { + public struct CommitResult: Sendable { + public init() { + + } } public struct AppPlaintext: Sendable, Equatable { diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index fbe01fc..b03430f 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -13,12 +13,10 @@ extension DiMLS { associatedtype Credential: DiMLSCredential //State Types - associatedtype Receiver: ReceiveChannel where Receiver.WelcomeOutput == WelcomeOutput + associatedtype Receiver: ReceiveChannel + where Receiver.WelcomeOutput.Credential == Credential associatedtype Sender: SendChannel where Sender.Credential == Credential - associatedtype WelcomeOutput: WelcomeOutputInterface - where WelcomeOutput.Credential == Credential - //Archivable var archive: Archive { get throws } @@ -41,7 +39,10 @@ extension DiMLS { //corresponding to a keyPackage //if we know we can process directly with the symmetric ratchet func received(privateMessage: Data) throws -> AppPlaintext - func received(ciphertext: Data) throws -> DecryptOutput + func received( + ciphertext: Data, + senderHint: DiMLS.ReferenceID? + ) throws -> DecryptOutput // func stageNewLocalKeyMaterial() throws } } @@ -170,7 +171,7 @@ extension DiMLS.DiGroup { private func commitIfNecessary( sender: Sender - ) throws -> DiMLS.CommitEffect? { + ) throws -> DiMLS.LocalCommitEffect? { guard pendingState.commitNeeded else { return nil } @@ -183,7 +184,7 @@ extension DiMLS.DiGroup { private func commit( input: DiMLS.CommitInput, sender: Sender, - ) throws -> DiMLS.CommitEffect { + ) throws -> DiMLS.LocalCommitEffect { var newRemotes: [DiMLS.CredentialedKeyPackage] = [] //modify remotes and group @@ -198,6 +199,7 @@ extension DiMLS.DiGroup { let sendChannel = try lazySender.readyChannel + //TODO: these are unused let (commitMessage, welcome) = try sendChannel.commit(input: input) if !newRemotes.isEmpty { @@ -219,7 +221,7 @@ extension DiMLS.DiGroup { ) } - public func received(welcome: WelcomeOutput) throws -> DiMLS.AppPlaintext? { + public func received(welcome: Receiver.WelcomeOutput) throws -> DiMLS.AppPlaintext? { guard welcome.diGroupId == diGroupId else { throw DiMLSError.mismatchedGroupId } diff --git a/Sources/DistributedMLS/Group/CommitEffect.swift b/Sources/DistributedMLS/Group/LocalCommitEffect.swift similarity index 83% rename from Sources/DistributedMLS/Group/CommitEffect.swift rename to Sources/DistributedMLS/Group/LocalCommitEffect.swift index 209e45f..8e8b957 100644 --- a/Sources/DistributedMLS/Group/CommitEffect.swift +++ b/Sources/DistributedMLS/Group/LocalCommitEffect.swift @@ -9,10 +9,10 @@ extension DiMLS { public struct EncryptResult { let privateMessage: PrivateMessage //application messages always follow a commit. let the application - let commitEffect: CommitEffect? + let commitEffect: LocalCommitEffect? } - public struct CommitEffect { + public struct LocalCommitEffect { let didIntroduceNewPubKey: Bool let localOps: [DiMLSOperations] //need to annotate the causal dependencies as well diff --git a/Sources/DistributedMLS/Group/ReceiveChannel.swift b/Sources/DistributedMLS/Group/ReceiveChannel.swift index 8d76017..b248656 100644 --- a/Sources/DistributedMLS/Group/ReceiveChannel.swift +++ b/Sources/DistributedMLS/Group/ReceiveChannel.swift @@ -11,4 +11,5 @@ public protocol ReceiveChannel { associatedtype WelcomeOutput: WelcomeOutputInterface static func create(welcome: WelcomeOutput) throws -> Self func decrypt(messageData: Data) throws -> DiMLS.AppPlaintext + func received(ciphertext: Data) throws -> DistributedMLS.DiMLS.DecryptOutput? } diff --git a/Sources/DistributedMLS/Helpers/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift index 19c0b3a..da3987b 100644 --- a/Sources/DistributedMLS/Helpers/Error.swift +++ b/Sources/DistributedMLS/Helpers/Error.swift @@ -15,6 +15,7 @@ public enum DiMLSError: Error { case disallowed case sendGroupNotReady case duplicateMember + case decryptFallthrough case expecting(DiMLS.Dependency) } @@ -28,6 +29,7 @@ extension DiMLSError: LocalizedError { case .disallowed: "Disallowed" case .sendGroupNotReady: "Send group not ready" case .duplicateMember: "Duplicate member" + case .decryptFallthrough: "Decryption fallthrough" case .expecting: "Missing dependency" } } From e7e6a2b7fb6f3e71088191d2c6f58829cc21e13a Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 21 Jan 2026 15:20:14 -0800 Subject: [PATCH 04/14] expand total group epoch to track dependencies --- .../TotalGroup/TotalGroup.swift | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 241770d..7a4bc3d 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -60,7 +60,7 @@ extension DiMLS { case .claimed(let epochs): let epoch = try epochs.last.tryUnwrap result[pair.key] = .credential( - epoch.credential, + epoch.senderCredential, epoch.epoch ) } @@ -73,7 +73,7 @@ extension DiMLS { } func welcomed(member: Membership.Epoch) throws { - let referenceId = member.credential.referenceId + let referenceId = member.senderCredential.referenceId guard case .invited = members[referenceId] else { throw DiMLSError.disallowed } @@ -114,32 +114,60 @@ extension DiMLS.TotalGroup { public struct Epoch: Archivable { let epoch: DiMLS.EpochID - public let credential: Credential + public let senderCredential: Credential - public init(epoch: DiMLS.EpochID, credential: Credential) { + let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + + //accumulate these and don't accept rollbacks + let baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + + public init( + epoch: DiMLS.EpochID, + senderCredential: Credential, + newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], + baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + ) { self.epoch = epoch - self.credential = credential + self.senderCredential = senderCredential + self.newDependencies = newDependencies + self.baseDependencies = baseDependencies } public init(archive: Archive) throws { self.init( epoch: archive.epoch, - credential: try .init(encoded: archive.credential) + senderCredential: try .init(encoded: archive.credential), + newDependencies: archive.newDependencies, + baseDependencies: archive.baseDependencies ) } public struct Archive: Codable, Sendable { let epoch: DiMLS.EpochID let credential: Data - - public init(epoch: DiMLS.EpochID, credential: Data) { + let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + let baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + + public init( + epoch: DiMLS.EpochID, + credential: Data, + newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], + baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] + ) { self.epoch = epoch self.credential = credential + self.newDependencies = newDependencies + self.baseDependencies = baseDependencies } } var archive: Archive { - .init(epoch: epoch, credential: credential.encoded) + .init( + epoch: epoch, + credential: senderCredential.encoded, + newDependencies: newDependencies, + baseDependencies: baseDependencies + ) } } @@ -160,7 +188,16 @@ extension DiMLS.TotalGroup.Membership: Archivable { credential: Data, epoch: UInt64 ) -> Self { - .claimed([.init(epoch: epoch, credential: credential)]) + .claimed( + [ + .init( + epoch: epoch, + credential: credential, + newDependencies: [:], + baseDependencies: [:] + ) + ] + ) } } From 9a2d7e6ef4898b7244a8107d67b097f0252a959a Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 21 Jan 2026 17:10:47 -0800 Subject: [PATCH 05/14] finish commit processing --- .../Group/Decrypt/DecryptOutput.swift | 16 ++++---- Sources/DistributedMLS/Group/DiGroup.swift | 41 +++++++++++++++++-- .../DistributedMLS/Group/ReceiveChannel.swift | 4 +- .../TotalGroup/TotalGroup.swift | 14 +++++++ 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index f7b82b3..5ef2d84 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -8,23 +8,25 @@ import Foundation extension DiMLS { - public struct DecryptOutput: Sendable { + public struct DecryptOutput: Sendable { public let appPlaintext: AppPlaintext - public let commitResult: CommitResult? + public let commitResult: CommitResult? public init( appPlaintext: AppPlaintext, - commitResult: CommitResult? + commitResult: CommitResult? ) { self.appPlaintext = appPlaintext self.commitResult = commitResult } } - public struct CommitResult: Sendable { - - public init() { - + public struct CommitResult: Sendable { + let added: [C] + let keyedDependency: KeyedDependency + public init(added: [C], keyedDependency: KeyedDependency) { + self.added = added + self.keyedDependency = keyedDependency } } diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index b03430f..641f960 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -39,10 +39,7 @@ extension DiMLS { //corresponding to a keyPackage //if we know we can process directly with the symmetric ratchet func received(privateMessage: Data) throws -> AppPlaintext - func received( - ciphertext: Data, - senderHint: DiMLS.ReferenceID? - ) throws -> DecryptOutput + // func stageNewLocalKeyMaterial() throws } } @@ -201,6 +198,7 @@ extension DiMLS.DiGroup { //TODO: these are unused let (commitMessage, welcome) = try sendChannel.commit(input: input) + pendingState.committed(input: input) if !newRemotes.isEmpty { @@ -245,6 +243,41 @@ extension DiMLS.DiGroup { .decrypt(messageData: appMessage) } + + public func received( + ciphertext: Data, + //helpful to have a hint here or you have to cycle through each send group + //as we use 1:1 channels this can be imferred from the channel + senderHint: DiMLS.ReferenceID? + ) throws -> DiMLS.DecryptOutput { + let matching: [Receiver] = try { + return if let senderHint { + [try receivers[senderHint].tryUnwrap] + } else { + .init(receivers.values) + } + }() + + for receiver in matching { + if let result = try receiver.received( + ciphertext: ciphertext, + diGroupId: diGroupId + ) { + if let commitResult = result.commitResult { + for added in commitResult.added { + try totalGroup.invited( + member: added.referenceId, + keyedDependency: commitResult.keyedDependency + ) + } + } + + return result + } + } + + throw DiMLSError.decryptFallthrough + } } //we have a generic (Credential) and a non-generic and could simplify them diff --git a/Sources/DistributedMLS/Group/ReceiveChannel.swift b/Sources/DistributedMLS/Group/ReceiveChannel.swift index b248656..3cd2ba2 100644 --- a/Sources/DistributedMLS/Group/ReceiveChannel.swift +++ b/Sources/DistributedMLS/Group/ReceiveChannel.swift @@ -11,5 +11,7 @@ public protocol ReceiveChannel { associatedtype WelcomeOutput: WelcomeOutputInterface static func create(welcome: WelcomeOutput) throws -> Self func decrypt(messageData: Data) throws -> DiMLS.AppPlaintext - func received(ciphertext: Data) throws -> DistributedMLS.DiMLS.DecryptOutput? + func received(ciphertext: Data, diGroupId: Data, ) throws -> DistributedMLS.DiMLS.DecryptOutput< + WelcomeOutput.Credential + >? } diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 7a4bc3d..9d28b28 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -79,6 +79,20 @@ extension DiMLS { } members[referenceId] = .claimed([member]) } + + func invited( + member: ReferenceID, + keyedDependency: DiMLS.KeyedDependency + ) throws { + if let existing = members[member] { + guard case .invited(let array) = existing else { + return + } + members[member] = .invited(array + [keyedDependency]) + } else { + members[member] = .invited([keyedDependency]) + } + } } } From 0e6cc10bc7c660ba21554a1da1828be068bd836d Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Thu, 22 Jan 2026 12:51:18 -0800 Subject: [PATCH 06/14] remove actor requirement on the DiGroup and Client --- Sources/DistributedMLS/Client.swift | 2 +- Sources/DistributedMLS/Group/DiGroup.swift | 62 +++++++++---------- .../DistributedMLS/Group/SendChannel.swift | 4 +- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/Sources/DistributedMLS/Client.swift b/Sources/DistributedMLS/Client.swift index 50ab870..9c2b2a1 100644 --- a/Sources/DistributedMLS/Client.swift +++ b/Sources/DistributedMLS/Client.swift @@ -8,7 +8,7 @@ import Foundation extension DiMLS { - public protocol Client: Actor, Archivable { + public protocol Client: Archivable { associatedtype Credential: DiMLSCredential associatedtype Group: DiGroup where Group.Receiver.WelcomeOutput == WelcomeOutput, Group.Credential == Credential diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 641f960..d710e18 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -9,7 +9,7 @@ import Foundation //higher level abstraction for the Local / extension DiMLS { - public protocol DiGroup: Actor, Archivable { + public protocol DiGroup: AnyObject, Archivable { associatedtype Credential: DiMLSCredential //State Types @@ -61,45 +61,39 @@ extension DiMLS.DiGroup { myCredential: Credential, credentialFetcher: @escaping CredentialKeyPackageFetcher, referenceIdFetcher: @escaping ReferenceIdKeyPackageFetcher - ) throws -> Task { + ) async throws { guard case .queued(let dependency) = lazySender else { - if case .creating(let task, _) = lazySender { - return task - } throw DiMLSError.sendGroupNotReady } - let task = Task { - do { - let archive = try await createSendGroup( - myCredential: myCredential, - members: - totalGroup - .membershipForCreating(sender: myCredential.referenceId), - dependency: dependency, - credentialFetcher: credentialFetcher, - referenceIdFetcher: referenceIdFetcher - ) - - let identityProvider = Sender.identityProvider( - totalGroup: totalGroup, - sender: myCredential - ) + lazySender = .creating(dependency) + do { + let archive = try await createSendGroup( + myCredential: myCredential, + members: + totalGroup + .membershipForCreating(sender: myCredential.referenceId), + dependency: dependency, + credentialFetcher: credentialFetcher, + referenceIdFetcher: referenceIdFetcher + ) + + let identityProvider = Sender.identityProvider( + totalGroup: totalGroup, + sender: myCredential + ) - lazySender = .ready( - try .init( - archive: archive, - diGroupId: diGroupId, - identityProvider: identityProvider - ) + lazySender = .ready( + try .init( + archive: archive, + diGroupId: diGroupId, + identityProvider: identityProvider ) - } catch { - print("error creating: \(error)") - lazySender = .queued(dependency) - throw error - } + ) + } catch { + print("error creating: \(error)") + lazySender = .queued(dependency) + throw error } - lazySender = .creating(task, dependency) - return task } private func createSendGroup( diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 6fde744..05c234f 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -80,7 +80,7 @@ public enum LazySendChannel { case ready(R) case queued(DiMLS.KeyedDependency?) //serves as a mutex on snapshot of Q state - case creating(Task, DiMLS.KeyedDependency?) + case creating(DiMLS.KeyedDependency?) public init( archive: Archive, @@ -124,7 +124,7 @@ extension LazySendChannel { try .ready(r.archive) case .queued(let dependency): .queued(dependency) - case .creating(_, let dependency): + case .creating(let dependency): .queued(dependency) } } From 89701a14c71ec59e56fe3cfac14c65889275df61 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Fri, 23 Jan 2026 15:15:39 -0800 Subject: [PATCH 07/14] externalize async portion of createSendCHannel --- Sources/DistributedMLS/Group/DiGroup.swift | 60 ++++++++++++------- .../DistributedMLS/Group/Participant.swift | 2 +- .../DistributedMLS/Group/SendChannel.swift | 13 +++- Sources/DistributedMLS/Helpers/Error.swift | 4 ++ 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index d710e18..d60bd54 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -24,6 +24,7 @@ extension DiMLS { //mutable object nonisolated var diGroupId: Data { get } + nonisolated var myReferenceId: Data { get } var totalGroup: DiMLS.TotalGroup { get } var receivers: [ReferenceID: Receiver] { get set } var pendingState: PendingState { get } @@ -57,24 +58,46 @@ extension DiMLS.DiGroup { return true } + public func prepareSendChannelSetup() + throws -> [DiMLS.ReferenceID: DiMLS.Participant] + { + let dependency: DiMLS.KeyedDependency? + switch lazySender { + //can call this repeatedly to get a newer membership list + case .preparing(let _dependency, _): + dependency = _dependency + case .queued(let _dependency): + dependency = _dependency + case .ready: + throw DiMLSError.sendGroupCreated + } + + let members = + try totalGroup + .membershipForCreating(sender: myReferenceId) + + lazySender = .preparing(dependency, members: members) + return members + } + public func setupChannel( myCredential: Credential, - credentialFetcher: @escaping CredentialKeyPackageFetcher, - referenceIdFetcher: @escaping ReferenceIdKeyPackageFetcher - ) async throws { - guard case .queued(let dependency) = lazySender else { + members: [DiMLS.ReferenceID: DiMLS.Participant], + keys: [DiMLS.ReferenceID: DiMLS.CredentialedKeyPackage] + ) throws { + guard case .preparing(let dependency, let _members) = lazySender else { throw DiMLSError.sendGroupNotReady } - lazySender = .creating(dependency) + guard members == _members else { + throw DiMLSError.reentrantCreateCall + } + do { - let archive = try await createSendGroup( + let archive = try createSendGroup( myCredential: myCredential, - members: - totalGroup - .membershipForCreating(sender: myCredential.referenceId), + members: members, + keys: keys, dependency: dependency, - credentialFetcher: credentialFetcher, - referenceIdFetcher: referenceIdFetcher ) let identityProvider = Sender.identityProvider( @@ -98,11 +121,10 @@ extension DiMLS.DiGroup { private func createSendGroup( myCredential: Credential, - members: [DiMLS.ReferenceID: DiMLS.Participant], + members: [DiMLS.ReferenceID: DiMLS.Participant], + keys: [DiMLS.ReferenceID: DiMLS.CredentialedKeyPackage], dependency: DiMLS.KeyedDependency?, - credentialFetcher: CredentialKeyPackageFetcher, - referenceIdFetcher: ReferenceIdKeyPackageFetcher - ) async throws -> Sender.Archive { + ) throws -> Sender.Archive { //capture a snapshot of what the group needs var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() @@ -113,18 +135,14 @@ extension DiMLS.DiGroup { for member in members { switch member.value { case .credential(let credential, let epoch): - let keyPackage = try await credentialFetcher(credential) remotes[member.key] = .init( - keyPackage: .init( - credential: credential, - keyPackage: keyPackage - ), + keyPackage: try keys[member.key].tryUnwrap, theirEpoch: epoch ) case .referenceId(let referenceId): remotes[member.key] = .init( - keyPackage: try await referenceIdFetcher(referenceId), + keyPackage: try keys[member.key].tryUnwrap, theirEpoch: nil ) } diff --git a/Sources/DistributedMLS/Group/Participant.swift b/Sources/DistributedMLS/Group/Participant.swift index dae50d7..7d704e8 100644 --- a/Sources/DistributedMLS/Group/Participant.swift +++ b/Sources/DistributedMLS/Group/Participant.swift @@ -9,7 +9,7 @@ import Foundation //exists for clients to reason about which credential to point to extension DiMLS { - public enum Participant { + public enum Participant: Equatable { case credential(Credential, DiMLS.EpochID) case referenceId(DiMLS.ReferenceID) } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 05c234f..913cc25 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -79,8 +79,15 @@ public struct SendChannelInputs { public enum LazySendChannel { case ready(R) case queued(DiMLS.KeyedDependency?) - //serves as a mutex on snapshot of Q state - case creating(DiMLS.KeyedDependency?) + + //we want to externalize the async fetching of keyPackages + //so we allow for the app to ask for a membership set, freezing it + //then come back async to init that version + //it is ok for us to miss some updates in the interim + case preparing( + DiMLS.KeyedDependency?, + members: [DiMLS.ReferenceID: DiMLS.Participant] + ) public init( archive: Archive, @@ -124,7 +131,7 @@ extension LazySendChannel { try .ready(r.archive) case .queued(let dependency): .queued(dependency) - case .creating(let dependency): + case .preparing(let dependency, _): .queued(dependency) } } diff --git a/Sources/DistributedMLS/Helpers/Error.swift b/Sources/DistributedMLS/Helpers/Error.swift index da3987b..ca2564e 100644 --- a/Sources/DistributedMLS/Helpers/Error.swift +++ b/Sources/DistributedMLS/Helpers/Error.swift @@ -14,6 +14,8 @@ public enum DiMLSError: Error { case notImplemented case disallowed case sendGroupNotReady + case sendGroupCreated + case reentrantCreateCall case duplicateMember case decryptFallthrough case expecting(DiMLS.Dependency) @@ -28,6 +30,8 @@ extension DiMLSError: LocalizedError { case .notImplemented: "Not implemented" case .disallowed: "Disallowed" case .sendGroupNotReady: "Send group not ready" + case .sendGroupCreated: "Send group already created" + case .reentrantCreateCall: "Reentrant create call" case .duplicateMember: "Duplicate member" case .decryptFallthrough: "Decryption fallthrough" case .expecting: "Missing dependency" From 2a3b8d213b3b5f62a62c7fcbff1aeb7b9117683c Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Fri, 23 Jan 2026 16:18:05 -0800 Subject: [PATCH 08/14] add participant referenceId --- Sources/DistributedMLS/Group/Participant.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/DistributedMLS/Group/Participant.swift b/Sources/DistributedMLS/Group/Participant.swift index 7d704e8..deceeb2 100644 --- a/Sources/DistributedMLS/Group/Participant.swift +++ b/Sources/DistributedMLS/Group/Participant.swift @@ -12,5 +12,14 @@ extension DiMLS { public enum Participant: Equatable { case credential(Credential, DiMLS.EpochID) case referenceId(DiMLS.ReferenceID) + + public var referenceId: DiMLS.ReferenceID { + switch self { + case .credential(let credential, _): + credential.referenceId + case .referenceId(let referenceID): + referenceID + } + } } } From 8047e87e7aefe4fd3eaccc35e76b79b3594ad3e0 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 24 Jan 2026 11:52:42 -0800 Subject: [PATCH 09/14] adjust for state introspection --- Sources/DistributedMLS/Group/SendChannel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index 913cc25..e98e7bf 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -45,6 +45,8 @@ public protocol SendChannel { //in the send channel func encrypt(plaintext: Data, authenticating: Data) throws -> [Credential: DiMLS.EncryptOutput] + var recipients: [Credential] { get throws } + } public struct SendChannelInputs { @@ -108,7 +110,7 @@ public enum LazySendChannel { } } - var readyChannel: R { + public var readyChannel: R { get throws { guard case .ready(let r) = self else { throw DiMLSError.sendGroupNotReady From 93c2964d8e6bcf84f1c9fa1ba5a67730fe24e54a Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sat, 24 Jan 2026 17:46:31 -0800 Subject: [PATCH 10/14] decrypt result needs a sender --- Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index 5ef2d84..7d382ef 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -22,7 +22,7 @@ extension DiMLS { } public struct CommitResult: Sendable { - let added: [C] + public let added: [C] let keyedDependency: KeyedDependency public init(added: [C], keyedDependency: KeyedDependency) { self.added = added @@ -33,10 +33,12 @@ extension DiMLS { public struct AppPlaintext: Sendable, Equatable { public let application: Data public let authenticating: Data + public let sender: Data - public init(application: Data, authenticating: Data) { + public init(application: Data, authenticating: Data, sender: Data) { self.application = application self.authenticating = authenticating + self.sender = sender } } } From fcefcc4acbea3b2d2afb3170c78a14f15e74c9d6 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Sun, 25 Jan 2026 17:10:10 -0800 Subject: [PATCH 11/14] add keyedDependency and ack structure to total group --- .../Group/Decrypt/DecryptOutput.swift | 2 +- Sources/DistributedMLS/Group/DiGroup.swift | 9 +++- .../Interfaces/WelcomeOutputInterface.swift | 3 +- .../TotalGroup/TotalGroup.swift | 49 +++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift index 7d382ef..f3fcecb 100644 --- a/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift +++ b/Sources/DistributedMLS/Group/Decrypt/DecryptOutput.swift @@ -23,7 +23,7 @@ extension DiMLS { public struct CommitResult: Sendable { public let added: [C] - let keyedDependency: KeyedDependency + public let keyedDependency: KeyedDependency public init(added: [C], keyedDependency: KeyedDependency) { self.added = added self.keyedDependency = keyedDependency diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index d60bd54..9480dcd 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -231,7 +231,10 @@ extension DiMLS.DiGroup { ) } - public func received(welcome: Receiver.WelcomeOutput) throws -> DiMLS.AppPlaintext? { + public func received( + welcome: Receiver.WelcomeOutput, + myCredential: Credential, + ) throws -> DiMLS.AppPlaintext? { guard welcome.diGroupId == diGroupId else { throw DiMLSError.mismatchedGroupId } @@ -246,7 +249,9 @@ extension DiMLS.DiGroup { throw DiMLSError.duplicateMember } receivers[senderReferenceId] = try .create(welcome: welcome) - try totalGroup.welcomed(member: try welcome.membershipEpoch) + try totalGroup.welcomed( + member: try welcome.membershipEpoch(myCredential: myCredential) + ) guard let appMessage = welcome.appPrivateMessage else { return nil diff --git a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift index 04d00e2..dfd0c5c 100644 --- a/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift +++ b/Sources/DistributedMLS/Interfaces/WelcomeOutputInterface.swift @@ -13,5 +13,6 @@ public protocol WelcomeOutputInterface: Sendable { var senderReferenceId: DiMLS.ReferenceID { get throws } var keyedDependency: DiMLS.KeyedDependency? { get } var appPrivateMessage: Data? { get } - var membershipEpoch: DiMLS.TotalGroup.Membership.Epoch { get throws } + func membershipEpoch(myCredential: Credential) throws + -> DiMLS.TotalGroup.Membership.Epoch } diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 9d28b28..c450f45 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -129,7 +129,11 @@ extension DiMLS.TotalGroup { public struct Epoch: Archivable { let epoch: DiMLS.EpochID public let senderCredential: Credential + public var recipients: [Recipient] + //can efface this when fully ack'd + var keyedDependency: DiMLS.KeyedDependency? + //acknowledge other groups let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] //accumulate these and don't accept rollbacks @@ -138,11 +142,15 @@ extension DiMLS.TotalGroup { public init( epoch: DiMLS.EpochID, senderCredential: Credential, + recipients: [Recipient], + keyedDependency: DiMLS.KeyedDependency?, newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] ) { self.epoch = epoch self.senderCredential = senderCredential + self.recipients = recipients + self.keyedDependency = keyedDependency self.newDependencies = newDependencies self.baseDependencies = baseDependencies } @@ -151,6 +159,9 @@ extension DiMLS.TotalGroup { self.init( epoch: archive.epoch, senderCredential: try .init(encoded: archive.credential), + recipients: try archive.recipients + .map { try .init(archive: $0) }, + keyedDependency: archive.keyedDependency, newDependencies: archive.newDependencies, baseDependencies: archive.baseDependencies ) @@ -159,17 +170,23 @@ extension DiMLS.TotalGroup { public struct Archive: Codable, Sendable { let epoch: DiMLS.EpochID let credential: Data + let recipients: [Recipient.Archive] + let keyedDependency: DiMLS.KeyedDependency? let newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] let baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] public init( epoch: DiMLS.EpochID, credential: Data, + recipients: [Recipient.Archive], + keyedDependency: DiMLS.KeyedDependency?, newDependencies: [DiMLS.ReferenceID: DiMLS.EpochID], baseDependencies: [DiMLS.ReferenceID: DiMLS.EpochID] ) { self.epoch = epoch self.credential = credential + self.recipients = recipients + self.keyedDependency = keyedDependency self.newDependencies = newDependencies self.baseDependencies = baseDependencies } @@ -179,6 +196,8 @@ extension DiMLS.TotalGroup { .init( epoch: epoch, credential: senderCredential.encoded, + recipients: recipients.map(\.archive), + keyedDependency: keyedDependency, newDependencies: newDependencies, baseDependencies: baseDependencies ) @@ -207,6 +226,8 @@ extension DiMLS.TotalGroup.Membership: Archivable { .init( epoch: epoch, credential: credential, + recipients: [], + keyedDependency: nil, newDependencies: [:], baseDependencies: [:] ) @@ -233,3 +254,31 @@ extension DiMLS.TotalGroup.Membership: Archivable { } } } + +extension DiMLS.TotalGroup.Membership.Epoch { + public struct Recipient: Archivable { + let credential: Credential + var acknowledged: Bool + + public init(credential: Credential, acknowledged: Bool) { + self.credential = credential + self.acknowledged = acknowledged + } + + public init(archive: Archive) throws { + self.init( + credential: try .init(encoded: archive.credential), + acknowledged: archive.acknowledged + ) + } + + public struct Archive: Codable, Sendable { + let credential: Data + let acknowledged: Bool + } + + var archive: Archive { + .init(credential: credential.encoded, acknowledged: acknowledged) + } + } +} From cb65db94412f57e6e32e44e364b894de1db09b0d Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 27 Jan 2026 01:27:06 -0800 Subject: [PATCH 12/14] multiple welcome dependencies --- .../DistributedMLS/Group/CommitInput.swift | 21 +++- Sources/DistributedMLS/Group/DiGroup.swift | 34 ++++++- .../DistributedMLS/Group/PendingState.swift | 98 +++++++++++++++---- .../DistributedMLS/Group/SendChannel.swift | 3 +- 4 files changed, 132 insertions(+), 24 deletions(-) diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index 1cecf38..5ddfeb6 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -10,7 +10,7 @@ import Foundation extension DiMLS { public struct CommitInput { public var localOps: Set> - public var followOps: Set> + public var followOps: [PendingState.FollowOp] public var dependencies: [KeyedDependency] @@ -45,3 +45,22 @@ extension DiMLS { } } } + +extension DiMLS { + //lookup table that we pass from total group to a client to process + public typealias AvailableDependendencies = [ReferenceID: [EpochID: KeyedDependency]] +} +extension DiMLS.TotalGroup { + public var keyedDependencies: DiMLS.AvailableDependendencies { + members.compactMapValues { + guard case .claimed(let epochs) = $0 else { + return nil + } + return epochs.reduce(into: [:]) { result, epoch in + if let keyedDependency = epoch.keyedDependency { + result[epoch.epoch] = keyedDependency + } + } + } + } +} diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 9480dcd..860c0c3 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -128,6 +128,7 @@ extension DiMLS.DiGroup { //capture a snapshot of what the group needs var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() + var dependencies = [dependency].compactMap(\.self) let members = try totalGroup @@ -148,18 +149,47 @@ extension DiMLS.DiGroup { } } + //queue up any pending adds as well + let adds = try pendingState.accumulateAdds() + if let adds { + for localOp in adds.localOps { + if case .add(let member) = localOp { + remotes[member.credential.referenceId] = .init( + keyPackage: member, + theirEpoch: nil + ) + } + } + + for followOp in adds.followOps { + if case .add(let member) = followOp.operation { + remotes[member.credential.referenceId] = .init( + keyPackage: member, + theirEpoch: nil + ) + if let dependecy = followOp.dependency { + dependencies.append(dependecy) + } + + } + } + pendingState.committed(input: adds) + } + return try Sender.create( input: .init( diGroupID: diGroupId, myCredential: myCredential, - remotes: remotes + remotes: remotes, + dependencies: dependencies ), identityProvider: Sender.identityProvider( totalGroup: totalGroup, sender: myCredential ), - dependency: dependency + dependencies: dependencies ) + } public func encryptWithCommits( diff --git a/Sources/DistributedMLS/Group/PendingState.swift b/Sources/DistributedMLS/Group/PendingState.swift index 56342f9..95c6c59 100644 --- a/Sources/DistributedMLS/Group/PendingState.swift +++ b/Sources/DistributedMLS/Group/PendingState.swift @@ -10,70 +10,128 @@ import os extension DiMLS { public final class PendingState { var applicationRequestsNewKeyDistribution: Bool - public var pendingLocalOps: Set> + public var localOps: Set> //this includes incorporating PCS updates - var pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] + var followOps: [FollowOp] public static func create() -> Self { .init( applicationRequestsNewKeyDistribution: false, - pendingLocalOps: [], - pendingFollowOps: [:] + localOps: [], + followOps: [] ) } init( applicationRequestsNewKeyDistribution: Bool, - pendingLocalOps: Set>, - pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations] + localOps: Set>, + followOps: [FollowOp] ) { self.applicationRequestsNewKeyDistribution = applicationRequestsNewKeyDistribution - self.pendingLocalOps = pendingLocalOps - self.pendingFollowOps = pendingFollowOps + self.localOps = localOps + self.followOps = followOps } //MARK: Archive public struct Archive: Sendable, Codable { let applicationRequestsNewKeyDistribution: Bool - let pendingLocalOps: [DiMLSOperations.Archive] + let localOps: [DiMLSOperations.Archive] //this includes incorporating PCS updates - let pendingFollowOps: [DiMLS.ReferenceID: DiMLSOperations.Archive] + let followOps: [FollowOp.Archive] } public init(archive: Archive) throws { self.applicationRequestsNewKeyDistribution = archive.applicationRequestsNewKeyDistribution - self.pendingLocalOps = try .init( - archive.pendingLocalOps.map { + self.localOps = try .init( + archive.localOps.map { try .init(archive: $0) }) - self.pendingFollowOps = try archive.pendingFollowOps - .mapValues { try .init(archive: $0) } + self.followOps = try archive.followOps + .map { try .init(archive: $0) } } public var archive: Archive { get throws { .init( applicationRequestsNewKeyDistribution: applicationRequestsNewKeyDistribution, - pendingLocalOps: try pendingLocalOps.map { try $0.archive }, - pendingFollowOps: try pendingFollowOps.mapValues { try $0.archive }, + localOps: try localOps.map { try $0.archive }, + followOps: + try followOps + .map { try $0.archive }, ) } } public var commitNeeded: Bool { - applicationRequestsNewKeyDistribution || !pendingLocalOps.isEmpty - || !pendingFollowOps.isEmpty + applicationRequestsNewKeyDistribution || !localOps.isEmpty + || !followOps.isEmpty } public func stageAdd(member: DiMLS.CredentialedKeyPackage) throws { - pendingLocalOps.insert(.add(member)) + localOps.insert(.add(member)) + } + + public func prepareCommit() throws -> DiMLS.CommitInput { + assert(commitNeeded) + var result = DiMLS.CommitInput() + //first follow remote ops + //TODO + + //then perform own actions if not redundant + for action in localOps { + result.localOps.insert(action) + } + + //inject causal dependency if needed + //TODO + + //broadcast new keys if necessary + //TODO + + return result } func committed(input: CommitInput) { for localOp in input.localOps { - pendingLocalOps.remove(localOp) + localOps.remove(localOp) } } + + public func accumulateAdds() throws -> DiMLS.CommitInput? { + //currently the same + if commitNeeded { + try prepareCommit() + } else { + nil + } + } + } +} + +extension DiMLS.PendingState { + public struct FollowOp { + let operation: DiMLSOperations + let dependency: DiMLS.KeyedDependency? + } +} + +extension DiMLS.PendingState.FollowOp: Archivable { + public struct Archive: Codable, Sendable { + let operation: DiMLSOperations.Archive + let dependency: DiMLS.KeyedDependency? + } + + public init(archive: Archive) throws { + self.init( + operation: try .init(archive: archive.operation), + dependency: archive.dependency + ) + } + + var archive: Archive { + get throws { + try .init(operation: try operation.archive, dependency: dependency) + } } } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index e98e7bf..aa1f34e 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -24,7 +24,7 @@ public protocol SendChannel { static func create( input: SendChannelInputs, identityProvider: IdentityProvider, - dependency: DiMLS.KeyedDependency? + dependencies: [DiMLS.KeyedDependency] ) throws -> Archive init( @@ -53,6 +53,7 @@ public struct SendChannelInputs { public let diGroupID: Data public let myCredential: Credential public let remotes: [DiMLS.ReferenceID: Remote] + public let dependencies: [DiMLS.KeyedDependency] public struct Remote { public let keyPackage: DiMLS.CredentialedKeyPackage From fe652487e277426a74c13648b238fd895e33e7a6 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 27 Jan 2026 15:43:16 -0800 Subject: [PATCH 13/14] revised group init --- Sources/DistributedMLS/Group/DiGroup.swift | 168 ++++++++---------- .../DistributedMLS/Group/SendChannel.swift | 63 +++++-- .../TotalGroup/TotalGroup.swift | 68 +++---- 3 files changed, 154 insertions(+), 145 deletions(-) diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 860c0c3..3ec891a 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -59,50 +59,97 @@ extension DiMLS.DiGroup { } public func prepareSendChannelSetup() - throws -> [DiMLS.ReferenceID: DiMLS.Participant] + throws -> SendChannelInputs.Snapshot { - let dependency: DiMLS.KeyedDependency? switch lazySender { //can call this repeatedly to get a newer membership list - case .preparing(let _dependency, _): - dependency = _dependency - case .queued(let _dependency): - dependency = _dependency case .ready: throw DiMLSError.sendGroupCreated + default: + break } - let members = - try totalGroup - .membershipForCreating(sender: myReferenceId) + //can't construct group de novo by unioning when joining, + //need to consult the existing groups + //to find the widest one to references + let referenceEpoch = try chooseReceiver() + let referenceSender = referenceEpoch.senderCredential + + var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote.Snapshot]() + remotes[referenceSender.referenceId] = + .joined( + epoch: referenceEpoch.epoch, + credential: referenceSender + ) + for recipient in referenceEpoch.recipients { + guard recipient.credential.referenceId != myReferenceId else { + continue + } - lazySender = .preparing(dependency, members: members) - return members + switch totalGroup.members[recipient.credential.referenceId] { + case .claimed(let epochs): + let epoch = try epochs.last.tryUnwrap + remotes[recipient.credential.referenceId] = + .joined( + epoch: epoch.epoch, + credential: epoch.senderCredential + ) + case .invited: + remotes[recipient.credential.referenceId] = + .invited + case .known: + remotes[recipient.credential.referenceId] = + .known(recipient.credential) + case .none: + throw DiMLSError.disallowed + } + } + + let snapshot = SendChannelInputs.Snapshot( + diGroupID: diGroupId, + remotes: remotes, + dependency: try referenceEpoch.keyedDependency.tryUnwrap + ) + lazySender = .preparing(snapshot) + return snapshot } - public func setupChannel( - myCredential: Credential, - members: [DiMLS.ReferenceID: DiMLS.Participant], - keys: [DiMLS.ReferenceID: DiMLS.CredentialedKeyPackage] - ) throws { - guard case .preparing(let dependency, let _members) = lazySender else { + private func chooseReceiver() throws -> DiMLS.TotalGroup.Membership.Epoch { + var choice: DiMLS.TotalGroup.Membership.Epoch? = nil + for (id, receiver) in totalGroup.members { + guard id != myReferenceId else { + continue + } + guard case .claimed(let epochs) = receiver else { + continue + } + let epoch = try epochs.last.tryUnwrap + if let _choice = choice { + if _choice.recipients.count < epoch.recipients.count { + choice = epoch + //TODO, if set equality, compare individual epochs + } + } else { + choice = epoch + } + } + return try choice.tryUnwrap + } + + public func setupChannel(input: SendChannelInputs) throws { + guard case .preparing(let stagedInput) = lazySender else { throw DiMLSError.sendGroupNotReady } - guard members == _members else { + guard input.snapshotId == stagedInput.id else { throw DiMLSError.reentrantCreateCall } do { - let archive = try createSendGroup( - myCredential: myCredential, - members: members, - keys: keys, - dependency: dependency, - ) + let archive = try createSendGroup(input: input) let identityProvider = Sender.identityProvider( totalGroup: totalGroup, - sender: myCredential + sender: input.myCredential ) lazySender = .ready( @@ -114,80 +161,19 @@ extension DiMLS.DiGroup { ) } catch { print("error creating: \(error)") - lazySender = .queued(dependency) + lazySender = .queued throw error } } - private func createSendGroup( - myCredential: Credential, - members: [DiMLS.ReferenceID: DiMLS.Participant], - keys: [DiMLS.ReferenceID: DiMLS.CredentialedKeyPackage], - dependency: DiMLS.KeyedDependency?, - ) throws -> Sender.Archive { - - //capture a snapshot of what the group needs - var remotes = [DiMLS.ReferenceID: SendChannelInputs.Remote]() - var dependencies = [dependency].compactMap(\.self) - - let members = - try totalGroup - .membershipForCreating(sender: myCredential.referenceId) - for member in members { - switch member.value { - case .credential(let credential, let epoch): - - remotes[member.key] = .init( - keyPackage: try keys[member.key].tryUnwrap, - theirEpoch: epoch - ) - case .referenceId(let referenceId): - remotes[member.key] = .init( - keyPackage: try keys[member.key].tryUnwrap, - theirEpoch: nil - ) - } - } - - //queue up any pending adds as well - let adds = try pendingState.accumulateAdds() - if let adds { - for localOp in adds.localOps { - if case .add(let member) = localOp { - remotes[member.credential.referenceId] = .init( - keyPackage: member, - theirEpoch: nil - ) - } - } - - for followOp in adds.followOps { - if case .add(let member) = followOp.operation { - remotes[member.credential.referenceId] = .init( - keyPackage: member, - theirEpoch: nil - ) - if let dependecy = followOp.dependency { - dependencies.append(dependecy) - } - - } - } - pendingState.committed(input: adds) - } - + private func createSendGroup(input: SendChannelInputs) throws -> Sender.Archive { return try Sender.create( - input: .init( - diGroupID: diGroupId, - myCredential: myCredential, - remotes: remotes, - dependencies: dependencies - ), + input: input, identityProvider: Sender.identityProvider( totalGroup: totalGroup, - sender: myCredential + sender: input.myCredential ), - dependencies: dependencies + dependency: input.dependency ) } diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index aa1f34e..b35c77b 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -24,7 +24,7 @@ public protocol SendChannel { static func create( input: SendChannelInputs, identityProvider: IdentityProvider, - dependencies: [DiMLS.KeyedDependency] + dependency: DiMLS.KeyedDependency? ) throws -> Archive init( @@ -50,10 +50,26 @@ public protocol SendChannel { } public struct SendChannelInputs { + public let snapshotId: UUID public let diGroupID: Data public let myCredential: Credential public let remotes: [DiMLS.ReferenceID: Remote] - public let dependencies: [DiMLS.KeyedDependency] + //for now, can only refer to a single group when distributing welcomes + public let dependency: DiMLS.KeyedDependency? + + public init( + snapshotId: UUID, + diGroupID: Data, + myCredential: Credential, + remotes: [DiMLS.ReferenceID: Remote], + dependency: DiMLS.KeyedDependency? + ) { + self.snapshotId = snapshotId + self.diGroupID = diGroupID + self.myCredential = myCredential + self.remotes = remotes + self.dependency = dependency + } public struct Remote { public let keyPackage: DiMLS.CredentialedKeyPackage @@ -67,6 +83,30 @@ public struct SendChannelInputs { self.keyPackage = keyPackage self.theirEpoch = theirEpoch } + + public enum Snapshot { + case invited + case known(Credential) + case joined(epoch: UInt64, credential: Credential) + + public var epoch: UInt64? { + switch self { + case .invited, .known: + nil + case .joined(let epoch): + epoch.epoch + } + } + } + } + + //get keypackages for these remotes and give me back a SendChannelInputs when done + public struct Snapshot { + public let id = UUID() + public let diGroupID: Data + public let remotes: [DiMLS.ReferenceID: Remote.Snapshot] + //for now, can only refer to a single group when distributing welcomes + public let dependency: DiMLS.KeyedDependency? } public var correspondingRecipientEpoch: [DiMLS.ReferenceID: UInt64] { @@ -81,16 +121,13 @@ public struct SendChannelInputs { public enum LazySendChannel { case ready(R) - case queued(DiMLS.KeyedDependency?) + case queued //we want to externalize the async fetching of keyPackages //so we allow for the app to ask for a membership set, freezing it //then come back async to init that version //it is ok for us to miss some updates in the interim - case preparing( - DiMLS.KeyedDependency?, - members: [DiMLS.ReferenceID: DiMLS.Participant] - ) + case preparing(SendChannelInputs.Snapshot) public init( archive: Archive, @@ -106,8 +143,8 @@ public enum LazySendChannel { identityProvider: identityProvider ) ) - case .queued(let dependency): - self = .queued(dependency) + case .queued: + self = .queued } } @@ -124,7 +161,7 @@ public enum LazySendChannel { extension LazySendChannel { public enum Archive: Sendable, Codable { case ready(R.Archive) - case queued(DiMLS.KeyedDependency?) + case queued } public var archive: Archive { @@ -132,10 +169,8 @@ extension LazySendChannel { switch self { case .ready(let r): try .ready(r.archive) - case .queued(let dependency): - .queued(dependency) - case .preparing(let dependency, _): - .queued(dependency) + case .queued, .preparing: + .queued } } } diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index c450f45..80b3144 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -44,29 +44,6 @@ extension DiMLS { // members.insert(member) // } - func membershipForCreating( - sender: ReferenceID - ) throws -> [ReferenceID: Participant] { - try members.reduce(into: [:]) { - result, - pair in - guard pair.key != sender else { - return - } - assert(result[pair.key] == nil) - switch pair.value { - case .invited(let dependencies): - result[pair.key] = .referenceId(pair.key) - case .claimed(let epochs): - let epoch = try epochs.last.tryUnwrap - result[pair.key] = .credential( - epoch.senderCredential, - epoch.epoch - ) - } - } - } - public func readyToWelcome(member: ReferenceID) throws { try members[member].tryUnwrap .readyToWelcome(member: member) @@ -118,6 +95,7 @@ extension DiMLS.TotalGroup { //can be empty so that as an identity provider it allows me to add them, //then lets me fill in the dependency case invited([DiMLS.KeyedDependency]) + case known //I did not see the initial invite case claimed([Epoch]) static func new( @@ -215,25 +193,26 @@ extension DiMLS.TotalGroup { extension DiMLS.TotalGroup.Membership: Archivable { public enum Archive: Codable, Sendable { case invited([DiMLS.KeyedDependency]) + case known case claimed([Epoch.Archive]) - public static func create( - credential: Data, - epoch: UInt64 - ) -> Self { - .claimed( - [ - .init( - epoch: epoch, - credential: credential, - recipients: [], - keyedDependency: nil, - newDependencies: [:], - baseDependencies: [:] - ) - ] - ) - } + // public static func create( + // credential: Data, + // epoch: UInt64 + // ) -> Self { + // .claimed( + // [ + // .init( + // epoch: epoch, + // credential: credential, + // recipients: [], + // keyedDependency: nil, + // newDependencies: [:], + // baseDependencies: [:] + // ) + // ] + // ) + // } } public init(archive: Archive) throws { @@ -242,6 +221,8 @@ extension DiMLS.TotalGroup.Membership: Archivable { self = .claimed(try epochs.map { try .init(archive: $0) }) case .invited(let dependencies): self = .invited(dependencies) + case .known: + self = .known } } @@ -251,6 +232,8 @@ extension DiMLS.TotalGroup.Membership: Archivable { .claimed(epochs.map(\.archive)) case .invited(let dependencies): .invited(dependencies) + case .known: + .known } } } @@ -275,6 +258,11 @@ extension DiMLS.TotalGroup.Membership.Epoch { public struct Archive: Codable, Sendable { let credential: Data let acknowledged: Bool + + public init(credential: Data, acknowledged: Bool) { + self.credential = credential + self.acknowledged = acknowledged + } } var archive: Archive { From 2924634f50e8ccbfa89ce9947db0a3ee73a96bcf Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Tue, 27 Jan 2026 23:28:21 -0800 Subject: [PATCH 14/14] fill in the total group on an incoming welcome --- .../DistributedMLS/Group/CommitInput.swift | 36 ++++---- Sources/DistributedMLS/Group/DiGroup.swift | 27 ++++-- .../DistributedMLS/Group/SendChannel.swift | 6 +- .../TotalGroup/TotalGroup.swift | 91 +++++++++++++------ 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/Sources/DistributedMLS/Group/CommitInput.swift b/Sources/DistributedMLS/Group/CommitInput.swift index 5ddfeb6..56489ea 100644 --- a/Sources/DistributedMLS/Group/CommitInput.swift +++ b/Sources/DistributedMLS/Group/CommitInput.swift @@ -46,21 +46,21 @@ extension DiMLS { } } -extension DiMLS { - //lookup table that we pass from total group to a client to process - public typealias AvailableDependendencies = [ReferenceID: [EpochID: KeyedDependency]] -} -extension DiMLS.TotalGroup { - public var keyedDependencies: DiMLS.AvailableDependendencies { - members.compactMapValues { - guard case .claimed(let epochs) = $0 else { - return nil - } - return epochs.reduce(into: [:]) { result, epoch in - if let keyedDependency = epoch.keyedDependency { - result[epoch.epoch] = keyedDependency - } - } - } - } -} +//extension DiMLS { +// //lookup table that we pass from total group to a client to process +// public typealias AvailableDependendencies = [ReferenceID: [EpochID: KeyedDependency]] +//} +//extension DiMLS.TotalGroup { +// public var keyedDependencies: DiMLS.AvailableDependendencies { +// members.compactMapValues { +// guard case .claimed(let epochs) = $0 else { +// return nil +// } +// return epochs.reduce(into: [:]) { result, epoch in +// if let keyedDependency = epoch.keyedDependency { +// result[epoch.epoch] = keyedDependency +// } +// } +// } +// } +//} diff --git a/Sources/DistributedMLS/Group/DiGroup.swift b/Sources/DistributedMLS/Group/DiGroup.swift index 3ec891a..289462a 100644 --- a/Sources/DistributedMLS/Group/DiGroup.swift +++ b/Sources/DistributedMLS/Group/DiGroup.swift @@ -36,10 +36,6 @@ extension DiMLS { //(add + dependency), so we let the implementation modify the pending //state to pop off the actions it can make progress on func prepareCommit() throws -> DiMLS.CommitInput - //different interface as it is initially handled by the init key - //corresponding to a keyPackage - //if we know we can process directly with the symmetric ratchet - func received(privateMessage: Data) throws -> AppPlaintext // func stageNewLocalKeyMaterial() throws } @@ -145,7 +141,8 @@ extension DiMLS.DiGroup { } do { - let archive = try createSendGroup(input: input) + let (archive, epoch) = try createSendGroup(input: input) + try totalGroup.welcomed(member: epoch) let identityProvider = Sender.identityProvider( totalGroup: totalGroup, @@ -166,7 +163,9 @@ extension DiMLS.DiGroup { } } - private func createSendGroup(input: SendChannelInputs) throws -> Sender.Archive { + private func createSendGroup( + input: SendChannelInputs + ) throws -> (Sender.Archive, DiMLS.TotalGroup.Membership.Epoch) { return try Sender.create( input: input, identityProvider: Sender.identityProvider( @@ -175,7 +174,6 @@ extension DiMLS.DiGroup { ), dependency: input.dependency ) - } public func encryptWithCommits( @@ -225,11 +223,10 @@ extension DiMLS.DiGroup { let sendChannel = try lazySender.readyChannel //TODO: these are unused - let (commitMessage, welcome) = try sendChannel.commit(input: input) + let (commitMessage, welcome, historyEpoch) = try sendChannel.commit(input: input) pendingState.committed(input: input) if !newRemotes.isEmpty { - for newRemote in newRemotes { let referenceId = newRemote.credential.referenceId @@ -239,6 +236,11 @@ extension DiMLS.DiGroup { } } + try totalGroup.committed( + referenceId: myReferenceId, + epoch: historyEpoch + ) + return .init( didIntroduceNewPubKey: input.newSenderLeafNode, localOps: [], @@ -277,6 +279,13 @@ extension DiMLS.DiGroup { } + public func received(privateMessage: Data, sender: DiMLS.ReferenceID) throws + -> DiMLS.AppPlaintext + { + try receivers[sender].tryUnwrap + .decrypt(messageData: privateMessage) + } + public func received( ciphertext: Data, //helpful to have a hint here or you have to cycle through each send group diff --git a/Sources/DistributedMLS/Group/SendChannel.swift b/Sources/DistributedMLS/Group/SendChannel.swift index b35c77b..86820d4 100644 --- a/Sources/DistributedMLS/Group/SendChannel.swift +++ b/Sources/DistributedMLS/Group/SendChannel.swift @@ -25,7 +25,7 @@ public protocol SendChannel { input: SendChannelInputs, identityProvider: IdentityProvider, dependency: DiMLS.KeyedDependency? - ) throws -> Archive + ) throws -> (Archive, DiMLS.TotalGroup.Membership.Epoch) init( archive: Archive, @@ -39,7 +39,8 @@ public protocol SendChannel { func commit(input: DiMLS.CommitInput) throws -> ( commitMessage: Commit, - welcomes: [Welcome] + welcomes: [Welcome], + historyEpoch: DiMLS.TotalGroup.Membership.Epoch ) //packaging the encrypted app message with metadata can all be done //in the send channel @@ -47,6 +48,7 @@ public protocol SendChannel { var recipients: [Credential] { get throws } + func exportDependencyKey(diGroupContext: Data) throws -> Data } public struct SendChannelInputs { diff --git a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift index 80b3144..4cdb938 100644 --- a/Sources/DistributedMLS/TotalGroup/TotalGroup.swift +++ b/Sources/DistributedMLS/TotalGroup/TotalGroup.swift @@ -37,13 +37,6 @@ extension DiMLS { members[member.referenceId] = .new(dependency: nil) } - // public func added(member: DiMLS.ReferenceID) throws { - // guard canAdd(member) else { - // throw DiMLSError.disallowed - // } - // members.insert(member) - // } - public func readyToWelcome(member: ReferenceID) throws { try members[member].tryUnwrap .readyToWelcome(member: member) @@ -54,7 +47,9 @@ extension DiMLS { guard case .invited = members[referenceId] else { throw DiMLSError.disallowed } - members[referenceId] = .claimed([member]) + + let existing = try members[referenceId].tryUnwrap + members[referenceId] = try existing.welcomed(epoch: member) } func invited( @@ -70,6 +65,14 @@ extension DiMLS { members[member] = .invited([keyedDependency]) } } + + func committed( + referenceId: DiMLS.ReferenceID, + epoch: Membership.Epoch + ) throws { + let existing = try members[referenceId].tryUnwrap + members[referenceId] = try existing.committed(epoch: epoch) + } } } @@ -187,6 +190,46 @@ extension DiMLS.TotalGroup { throw DiMLSError.duplicateMember } } + + func welcomed(epoch: Epoch) throws -> Membership { + switch self { + case .known, .invited: + break + case .claimed(let array): + throw DiMLSError.disallowed + } + + return .claimed([epoch]) + + } + + func committed(epoch: Epoch) throws -> Membership { + guard case .claimed(let epochs) = self else { + throw DiMLSError.disallowed + } + let newest = try epochs.last.tryUnwrap + assert(newest.epoch + 1 == epoch.epoch) + + var newBase = newest.baseDependencies + for (key, value) in epoch.newDependencies { + if let existing = newBase[key] { + assert(value > existing) + newBase[key] = value + } + } + + let newEpoch = Epoch( + epoch: epoch.epoch, + senderCredential: epoch.senderCredential, + recipients: epoch.recipients, + keyedDependency: epoch.keyedDependency, + newDependencies: epoch.newDependencies, + baseDependencies: epoch.baseDependencies + ) + + return .claimed(epochs + [epoch]) + + } } } @@ -195,24 +238,6 @@ extension DiMLS.TotalGroup.Membership: Archivable { case invited([DiMLS.KeyedDependency]) case known case claimed([Epoch.Archive]) - - // public static func create( - // credential: Data, - // epoch: UInt64 - // ) -> Self { - // .claimed( - // [ - // .init( - // epoch: epoch, - // credential: credential, - // recipients: [], - // keyedDependency: nil, - // newDependencies: [:], - // baseDependencies: [:] - // ) - // ] - // ) - // } } public init(archive: Archive) throws { @@ -270,3 +295,17 @@ extension DiMLS.TotalGroup.Membership.Epoch { } } } + +extension DiMLS.TotalGroup { + public var knownDependencies: [DiMLS.KeyedDependency] { + members.values.reduce(into: []) { result, member in + switch member { + case .invited(let dependencies): + result += dependencies + case .known: break + case .claimed(let epochs): + result += epochs.compactMap(\.keyedDependency) + } + } + } +}