Skip to content

feat: XIP-82 review revisions — typed invite primitives, max_uses, refresh_pointers, AAD + effective expiry, admitted_via tag#336

Open
tylerhawkes wants to merge 1 commit into
mainfrom
tyler/external-invite-xip82-revisions
Open

feat: XIP-82 review revisions — typed invite primitives, max_uses, refresh_pointers, AAD + effective expiry, admitted_via tag#336
tylerhawkes wants to merge 1 commit into
mainfrom
tyler/external-invite-xip82-revisions

Conversation

@tylerhawkes

@tylerhawkes tylerhawkes commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #334, mirroring the review-revised XIP-82 (xmtp/XIPs#140). Wire note up front: wrapping bytes in a submessage is a wire-format change, but the raw layout never shipped (#334 is unreleased), so there is no compatibility constraint to honor.

external_invite.proto

  • Typed nominal newtypesSymmetricKey {material}, GroupStateHash {digest}, ServicePointer {oneof https_url | opaque} — replace raw bytes. No algorithm enums: the versioned envelope pins the AEAD, the group's ciphersuite pins the hash, so an in-message tag could only contradict them. Length rules (32-byte key, ciphersuite-length digest) are validator/setter checks, documented on the types.
  • ExternalInvitePayloadV1.service_pointerServicePointer, MAY be absent (application-resolved service; keeps the fetch target out of the QR). Present-but-empty oneof = parse failure (fail closed).
  • EncryptedGroupInfoBlobV1:
    • v1 AAD = epoch || expires_at_ns (8-byte BE each) || group_state_hash.digest — all plaintext metadata is authenticated, so a keyless writer can't doctor a captured blob (e.g. extend its expiry).
    • expires_at_ns is redefined as the uploader-computed effective expiry: min(policy.expires_at_ns, epoch_start + policy.expire_in_ns) (0 = no bound drops out). The joiner's single expiry check then also skips candidates validators would reject as stale — no zombie joins — and service TTL-GC collects staleness-dead blobs.
    • Nonces MUST be CSPRNG-random (many independent writers share one long-lived key; counter schemes collide across writers).
    • Service-contract comments rewritten: versioned slot (retain last N FIFO, never evict by uploader-asserted epoch, coalesce only byte-identical uploads) replaces the old "strictly newer epoch wins" ordering, which let a single forged epoch = u64::MAX upload permanently wedge a slot.

external_commit_policy.proto

  • symmetric_keySymmetricKey (absent = the only cleared encoding; empty submessage / empty material invalid).
  • New uint32 max_uses = 6: concurrent per-invite cap, counted as the number of live GROUP_MEMBERSHIP entries tagged with the active external_group_id — state-derived, so every member (including post-issuance joiners) converges. Durable setting.
  • New repeated ServicePointer refresh_pointers = 7: locations members use to keep the blob fresh across epoch advances (jittered check-before-write; see XIP-82 "Keeping the blob fresh"). Optional — empty preserves pointer secrecy at a liveness cost. Per-invite field.
  • Invariant comments rewritten to the revised lifecycle: fields split into durable settings (expire_in_ns, max_uses) vs per-invite (symmetric_key, external_group_id, expires_at_ns, refresh_pointers); revoke leaves every per-invite field absent (validator-enforced post-state invariant — a revoked policy is byte-identical to one that never had an invite); enable couples the GROUP_MEMBERSHIP external_committer_permissions grant in the same commit; fresh-CSPRNG-key-on-every-enable replaces key-history tracking; both time bounds are evaluated against delivery-service envelope timestamps (never validator wall-clock).

group_membership.proto

  • GroupMembershipEntry.V1 gains bytes admitted_via_external_group_id = 3: the max_uses accounting substrate. Recorded on every external commit (even max_uses = 0), write-once (member-sender commits that set/clear/alter it are rejected; entry rewrites must carry it through), absent for Welcome-added members.

Consumers

libxmtp picks this up via an xmtp_proto re-bump; the consuming changes land as follow-ups on the open QR-invite PR stack (xmtp/libxmtp#3671 → #3666 → #3667 → #3668 → #3673 → #3674).

🤖 Generated with Claude Code

Note

Add typed invite primitives, max_uses, refresh_pointers, AAD fields, and admitted_via tag for XIP-82

  • Introduces structured message types SymmetricKey, GroupStateHash, and ServicePointer in external_invite.proto, replacing raw bytes fields across ExternalInvitePayloadV1 and EncryptedGroupInfoBlobV1.
  • Updates ExternalCommitPolicyV1 in external_commit_policy.proto to use SymmetricKey for symmetric_key, and adds max_uses (uint32) and refresh_pointers (repeated ServicePointer) fields.
  • Adds admitted_via_external_group_id (bytes, write-once) to GroupMembershipEntry.V1 in group_membership.proto to tag members admitted via an external commit.
  • Risk: symmetric_key type changes from bytes to SymmetricKey and service_pointer/group_state_hash similarly change from bytes to structured messages — wire-incompatible with any prior serialized messages using these fields.

Macroscope summarized c78e379.

Review notes (self-review)

  • Verified: no message-name collisions across the xmtp.mls.message_contents package; the new external_commit_policy.proto → external_invite.proto import is acyclic and resolves under buf build; field numbers are additive; protolint-clean (≤80 cols, existing style).
  • Known pre-existing gap (since feat: external invite + ExternalCommitPolicy component + external_committer_permissions #334, unchanged here): dev/ts/generate and ts/index.ts do not include external_invite.proto / external_commit_policy.proto / component_permissions.proto, so the TS package has never exposed these types. Out of scope for this PR — the consumer is libxmtp (Rust); worth a tiny follow-up when the JS SDK needs invites.

@tylerhawkes tylerhawkes requested a review from a team as a code owner June 9, 2026 22:25
@macroscopeapp

macroscopeapp Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new protocol message types, new fields with validation semantics (max_uses admission limits, admitted_via tracking), and wire-format changes (bytes to typed messages). These schema changes define new feature capabilities and enforcement logic that warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@tylerhawkes tylerhawkes force-pushed the tyler/external-invite-xip82-revisions branch 2 times, most recently from 5c762f0 to 160192a Compare June 9, 2026 23:18
tylerhawkes added a commit to xmtp/libxmtp that referenced this pull request Jun 11, 2026
…ves) + adapt invite::payload

Regenerates xmtp_proto from xmtp/proto#336 (typed SymmetricKey /
GroupStateHash / ServicePointer newtypes, ExternalCommitPolicyV1
max_uses + refresh_pointers, EncryptedGroupInfoBlobV1 AAD +
effective-expiry semantics, GroupMembershipEntry.V1
admitted_via_external_group_id).

Consuming changes kept to what main already contains:

- invite::payload rewritten for the typed shape: symmetric_key is a
  SymmetricKey submessage (MissingSymmetricKey + material-length
  checks), service_pointer is an optional ServicePointer (absent =
  application-resolved; present-but-empty oneof fails closed;
  https_url parsed and scheme-checked via url). New
  https_service_pointer / opaque_service_pointer /
  validate_service_pointer helpers.

- GroupMembershipEntry.V1 construction sites gain the new
  admitted_via_external_group_id field, empty everywhere: the
  migrator's synthesized entries are all Welcome/legacy members
  (absent is permanently correct), and the membership-update rewrite
  path documents that tag preservation MUST land together with the
  validator's write-once enforcement in the external-commit stack —
  nothing on main can set the tag yet, so empty is correct today.

The rest of the QR-invite stack (#3671 -> #3666 -> #3667 -> #3668 ->
#3673 -> #3674) rebases onto this and picks up the new fields layer
by layer.
allow_external_commit itself authorizes the joiner-self-entry insert,
so the enable is a single-component write again and the multi-update
intent extension comes out entirely (it never existed on main).
external_committer_permissions comments reframed as the reserved
deny-by-default surface for writes beyond the v1 atomic shape.
@tylerhawkes tylerhawkes force-pushed the tyler/external-invite-xip82-revisions branch from 26de115 to c78e379 Compare June 11, 2026 04:53
tylerhawkes added a commit to xmtp/libxmtp that referenced this pull request Jun 11, 2026
…ves) + adapt invite::payload

Regenerates xmtp_proto from xmtp/proto#336 (typed SymmetricKey /
GroupStateHash / ServicePointer newtypes, ExternalCommitPolicyV1
max_uses + refresh_pointers, EncryptedGroupInfoBlobV1 AAD +
effective-expiry semantics, GroupMembershipEntry.V1
admitted_via_external_group_id).

Consuming changes kept to what main already contains:

- invite::payload rewritten for the typed shape: symmetric_key is a
  SymmetricKey submessage (MissingSymmetricKey + material-length
  checks), service_pointer is an optional ServicePointer (absent =
  application-resolved; present-but-empty oneof fails closed;
  https_url parsed and scheme-checked via url). New
  https_service_pointer / opaque_service_pointer /
  validate_service_pointer helpers.

- GroupMembershipEntry.V1 construction sites gain the new
  admitted_via_external_group_id field, empty everywhere: the
  migrator's synthesized entries are all Welcome/legacy members
  (absent is permanently correct), and the membership-update rewrite
  path documents that tag preservation MUST land together with the
  validator's write-once enforcement in the external-commit stack —
  nothing on main can set the tag yet, so empty is correct today.

The rest of the QR-invite stack (#3671 -> #3666 -> #3667 -> #3668 ->
#3673 -> #3674) rebases onto this and picks up the new fields layer
by layer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant