Group Manifest
The Group Manifest is the signed list of a Sync Group’s members. Every member holds a copy and checks every incoming message against it. Any member can issue a new one. When two members change the group at the same moment, the one whose change lost re-applies it.
This page describes the manifest as the Developer Preview decides it. Membership explains what it means for an app.
Fields
group_id: a random identifier, fixed when the group is created.version: one higher than the manifest this one was built on.parent: the hash of the manifest this one was built on. Empty in the first manifest of a group.members: every member’s X25519 key, Ed25519 key and name.issued_by: the Ed25519 key of the member who issued it.signature: Ed25519, byissued_by, over a domain-separated input that includes the End-to-End Version,group_id,version,parentandmembers.
A member’s name is a label derived from its signing key, two words such as SwiftFox. It is for display only. Names are short enough that someone can generate a key whose name matches a real device, so a name never proves which device is which.
A manifest travels as a GroupManifest message inside an ordinary sealed Envelope, one per member, through the same outbox as every other message. Manifest messages never expire from the outbox. The relay can’t read the message type, though it sees a manifest go out to every member at once.
A group holds at most 32 members, so that the largest manifest still fits in one frame. An admission that would go over fails with groupFull{max}.
Accepting a manifest
A member accepts an incoming manifest only if all of these hold:
- the signature verifies against
issued_by; group_idmatches its own group;issued_byis a member of the manifest it currently holds;- it orders after its current manifest by
(version, hash), which means a higher version, or the same version and a lower hash; - the version jump is within a sanity bound.
A manifest that fails is acknowledged, dropped and reported as a delivery issue.
A device in no group accepts a manifest only while it has a Pending Join. It then accepts its first manifest only if the signing key from the Pairing Token signed it, signed the Envelope it came in, and the device’s own keys are among the members. The group ID and version can be anything, because a device can join an existing group at any version. Pairing covers the joining side.
Concurrent changes
Two members can change the group at the same moment, each building on the same manifest. Every device orders the two the same way, by (version, hash), so every device ends up holding the same one. The issuer of the losing change re-applies it, except in the cases below.
When a member admits, removes or leaves, it records a Pending Membership Change in the same transaction as the new manifest and its outbox entries. The change holds its intent, such as “D is a member”, and the hash of the manifest that carried it. Each time the issuer accepts a newer manifest, it checks each pending change:
| The newer manifest | What the issuer does |
|---|---|
| Already reflects the intent | Clears the change |
| Descends from a manifest that carried the change | Clears it. A later, deliberate change undid it. |
| Neither, so it was built concurrently | Re-applies the change on top, as a new manifest one version higher |
| Leaves the issuer out | Drops the change, because the issuer is no longer a member |
If re-applying an admission would take the group over the Maximum Group Size (32 members), the issuer drops the change instead and reports admissionDropped(name, reason). The joiner stays in Pending Join.
Pending Membership Changes survive a restart. An issuer that was offline re-applies after it reconnects.
The promise, which the preview tests:
Once every member has received the same set of Group Manifests, all members hold an identical manifest. A membership change from an honest member is never lost, unless that member was itself removed concurrently.
While a change is being re-applied, a device can see a member leave and come back. Apps render the current member list and don’t treat a single membersChanged event as final.
Removal and leaving
A device counts itself removed only when the manifest that leaves it out has a parent the device holds, or has held, that includes it. It then wipes its group state, outbox and handled Message IDs, generates a fresh identity, and reports statusChanged(notJoined, removed).
When a manifest that leaves the device out was built concurrently, on a manifest that never included it, the device treats it as a race it lost. It keeps that manifest as its current view, stays alive, and waits for its admitter to re-apply the admission. A new member whose admission lost a tie is the usual case.
A leaving device issues a manifest without itself and moves to leaving. It stays connected until the relay has accepted every one of its manifest pushes, then wipes in the same way and reports statusChanged(notJoined, left).
Trust
Any current member can change the manifest, and every current member is fully trusted for membership. A hostile member can remove everyone else. A removed device that ignores its removal can still change membership on devices that haven’t yet seen it removed. TrueSeal doesn’t defend against either. The answer to a lost or hostile device is Destroy Group.
The relay holds no manifest and keeps no record of groups. It routes each manifest Envelope by its recipient key like any other. It can still guess which devices share a group from which devices receive messages together.
Where today’s code differs
Today’s manifest has no parent. A device keeps whichever equal-version manifest it saw first, so a concurrent change can be undone without anyone knowing. There is no Pending Membership Change, no leave operation, no group size cap and no bound on version jumps. The format above is decided in sync ADR-0027 and isn’t built yet.