ITADN

use GroupId Everywhere

#3550Openinsipx 创建于 2026-04-23
I
insipxcommented
precursor to #2227 # GroupId Type Migration Design Date: 2026-04-23 Status: Draft ## Goal Adopt `xmtp_proto::types::ids::GroupId` as the canonical group identifier throughout the libxmtp workspace, replacing raw `Vec<u8>` / `&[u8]` usage at application, repo, and subscription layers. Keep the type ergonomic to interoperate with `openmls::group::GroupId`. This migration is phase 1 of a two-phase effort. Phase 2 (separate future PR) swaps the internal representation to `[u8; 16]` and makes `GroupId` `Copy`. Phase 1 design decisions must not block that swap. ## Non-Goals - Phase 2 internal representation change to `[u8; 16]`. - Diesel `ToSql`/`FromSql` impls for `GroupId` (deferred to a later PR). - Tightening conversion surface (e.g. infallible → `TryFrom`). Deferred until phase 2. - Changing the 16-byte group-id generation logic in MLS group creation. - Schema or migration changes in `xmtp_db`. ## Current State `GroupId` already exists at `crates/xmtp_proto/src/types/ids/group_id.rs`. It wraps `bytes::Bytes` and exposes: - `From<Vec<u8>>`, `From<&[u8]>` (infallible). - `FromStr` (hex decode). - `AsRef<[u8]>`, `Borrow<[u8]>`, `Deref<Target = Bytes>`. - `Display` (hex), `Debug` (truncated hex). - `xmtp_common::Generate` under `if_test!`. `xmtp_proto` already depends on `openmls` via workspace. No new dep needed. Usage survey (approximate, from ripgrep): - 130 sites already type `group_id` as `GroupId` / `&GroupId`. - 236 sites still type it as `Vec<u8>` / `&[u8]` / `Bytes`. - 21 sites call `openmls_group.group_id()` directly and feed the resulting `openmls::group::GroupId` into raw-bytes APIs. - openmls-imported `GroupId` appears in 5 files today: `sql_key_store.rs`, `test/mock/openmls_mock.rs`, `groups/oneshot.rs`, `export_stream/group_save.rs`, `groups/mls_ext/commit_log_storer.rs`. Group ids are generated as `xmtp_common::rand_vec::<16>()` today — the 16-byte constraint is established by convention but not enforced at the type level. ## Design Decisions ### 1. Scope — full replacement at application layer `xmtp_proto::GroupId` is used throughout `xmtp_mls`, `xmtp_db` (repo layer), `xmtp_api`, `xmtp_api_d14n`, and `xmtp_archive`. `openmls::group::GroupId` is confined to code that directly calls openmls APIs (MLS sync, MLS extensions, openmls key store, MLS-aware archive code, openmls mocks). Conversion happens at that boundary. Why: xmtp_proto stays MLS-free conceptually even though it holds the openmls dep. Application code speaks one canonical type. The 236 raw-bytes sites collapse to the single `GroupId` type. ### 2. Internal representation — keep `bytes::Bytes` for now `GroupId` continues to wrap `bytes::Bytes` in this phase. Phase 2 swaps the inner representation to `[u8; 16]` and adds `Copy`. Why: deferring the internal change keeps phase 1 mechanical (type swaps, no bytes-handling logic changes). The phase 2 swap should be non-breaking at API level if phase 1 avoids leaking the `Bytes` internal representation to new consumers. **Design constraint driven by phase 2:** all new APIs added in phase 1 must not rely on `Deref<Target = Bytes>`. The existing `Deref<Bytes>` impl stays for back-compat with currently-compiling code, but new call sites use `.as_slice()` / `AsRef<[u8]>` only. Where an existing site relies on `Deref<Bytes>` specifically (not `[u8]`), rewrite it to `AsRef<[u8]>` during migration so phase 2 can remove `Deref`. ### 3. Name collision with `openmls::group::GroupId` — rename openmls on import In files touching both types, import as: ```rust use openmls::group::GroupId as OpenMlsGroupId; ``` `GroupId` unqualified always means `xmtp_proto::types::GroupId`. The handful of files (≤10 after migration) importing the openmls type follow this convention uniformly. ### 4. Conversion ergonomics — `From` impls + inherent method #### Orphan rule analysis Rust's orphan rule (`impl T for X` in crate `C` requires `T` local to `C` OR `X` local to `C`, with generic-parameter constraints): - **`impl From<&openmls::group::GroupId> for GroupId`** in xmtp_proto — target `GroupId` is local. **Allowed.** - **`impl From<openmls::group::GroupId> for GroupId`** in xmtp_proto — target `GroupId` is local. **Allowed.** - **`impl From<&GroupId> for openmls::group::GroupId`** in xmtp_proto — `From` is foreign (core), target `openmls::group::GroupId` is foreign. The *generic parameter* being local is not sufficient — the orphan rule keys off the `Self` (target) type. **Blocked.** So `From`/`Into` works **inbound** (openmls → xmtp) but not **outbound** (xmtp → openmls). The outbound direction must be an inherent method on `GroupId`. #### Impls ```rust impl From<&openmls::group::GroupId> for GroupId { fn from(id: &openmls::group::GroupId) -> Self { GroupId::from(id.as_slice()) } } impl From<openmls::group::GroupId> for GroupId { fn from(id: openmls::group::GroupId) -> Self { GroupId::from(id.as_slice()) } } impl GroupId { pub fn to_openmls(&self) -> openmls::group::GroupId { openmls::group::GroupId::from_slice(self.as_ref()) } } ``` #### Call-site ergonomics ```rust let xmtp_id: GroupId = mls_group.group_id().into(); // From (inbound) let ommls_id: openmls::group::GroupId = xmtp_id.to_openmls(); // inherent (outbound) ``` Raw-bytes inbound already works via existing `From<&[u8]>` / `From<Vec<u8>>`. No extension trait needed. ### 4a. Random generation — `GroupId::random` via `OpenMlsRand` Mirror `openmls::group::GroupId::random` on the xmtp_proto `GroupId`. Reuses the MLS provider's RNG so group-id bytes come from the same CSPRNG openmls uses internally, and matches the 16-byte length. ```rust use openmls_traits::random::OpenMlsRand; impl GroupId { pub fn random<R: OpenMlsRand>(rand: &R) -> Self { let bytes: [u8; 16] = rand .random_array() .expect("OpenMlsRand failed to produce randomness"); GroupId::from(bytes.as_slice()) } } ``` (`rand.random_array::<16>()` is the openmls API shape; implementation follows `openmls/src/group/mod.rs` verbatim.) Group creation then reads: ```rust let group_id = GroupId::random(provider.rand()); let mls_group = MlsGroup::new_with_group_id( provider, &identity.installation_keys, group_config, group_id.to_openmls(), credential_with_key, )?; ``` Replaces `xmtp_common::rand_vec::<16>()` at MLS-group-creation sites. Other `rand_vec::<16>()` usages (non-group-id) stay as-is. Phase 2 alignment: when the internal representation becomes `[u8; 16]`, this `random` impl collapses to `GroupId(rand.random_array().expect(...))` without any call-site change. `xmtp_proto` already depends on `openmls_traits` transitively via `openmls`; if it does not re-export `OpenMlsRand`, add an explicit `openmls_traits` workspace dep to xmtp_proto. Verify during implementation. ### 5. Conversion surface with raw bytes — unchanged Keep existing infallible `From<Vec<u8>>`, `From<&[u8]>`, `FromStr`, `AsRef<[u8]>`, `Borrow<[u8]>`, `Deref<Bytes>`. Phase 2 will replace infallible conversions with checked ones when the `[u8; 16]` invariant becomes enforced at the type level. Migration sites in phase 1 use `From`/`Into` freely, e.g. `let id: GroupId = vec_bytes.into();`. ### 6. DB schema strategy — repo-layer conversion, Diesel models unchanged Diesel model structs continue to hold `Vec<u8>` group_id fields in this phase. Repo functions accept and return `GroupId`, converting to/from `Vec<u8>` at the Diesel-call boundary inside the function body. Example: ```rust pub fn find_group(&self, id: &GroupId) -> Result<Option<GroupRecord>, _> { groups::table .filter(groups::id.eq(id.as_ref())) .first::<RawGroupRecord>(conn) .optional() .map(|opt| opt.map(GroupRecord::from)) } ``` A later PR introduces Diesel `ToSql`/`FromSql` impls for `GroupId` and swaps the struct fields. ## Architecture ### Module layout ``` xmtp_proto::types::ids ├── group_id.rs (GroupId + OpenMlsGroupIdExt + to_openmls) └── installation_id.rs (unchanged) ``` Export path: - `xmtp_proto::types::GroupId` (existing, unchanged). - `xmtp_proto::types::ids::OpenMlsGroupIdExt` (new). - Re-export the trait from `xmtp_proto::prelude` so `use xmtp_proto::prelude::*` brings it in. ### Conversion boundaries ``` Network (protobuf Vec<u8>) │ boundary: extractors / impls in xmtp_api_d14n, xmtp_api ▼ xmtp_proto::GroupId ◄──── canonical type ────► application layer │ │ │ boundary: repo fns in xmtp_db │ boundary: to_openmls() / to_xmtp() ▼ ▼ Diesel Vec<u8> (internal) openmls::group::GroupId ``` Three boundaries, each handled uniformly: 1. **Network boundary.** Protobuf-generated `Vec<u8>` group_id fields convert to `GroupId` in the API-layer extractors / response parsers. Outgoing requests do `.as_ref().to_vec()` when the protobuf encoder demands `Vec<u8>`. 2. **DB boundary.** Diesel queries pass `id.as_ref()` for reads/writes of `Vec<u8>` columns. Repo return types wrap raw rows into domain types holding `GroupId`. 3. **openmls boundary.** `mls_group.group_id().to_xmtp()` inbound, `group_id.to_openmls()` outbound. ### Migration order Crates migrated bottom-up so downstream crates see `GroupId` in their dependencies before they flip themselves: 1. `xmtp_proto` — add `OpenMlsGroupIdExt`, `to_openmls()` inherent method. Prelude re-export. 2. `xmtp_db` — repo functions + domain structs adopt `GroupId`. Diesel struct fields stay `Vec<u8>`. 3. `xmtp_api` / `xmtp_api_d14n` — trait signatures, extractors, query structs. 4. `xmtp_mls` — `MlsGroup::group_id` field, all group-id-receiving methods, subscription streams, mls_sync boundary conversions. Replace `xmtp_common::rand_vec::<16>()` at group-creation sites with `GroupId::random(provider.rand())`; call `.to_openmls()` when invoking `MlsGroup::new_with_group_id`. 5. `xmtp_archive` — archive export/import paths. 6. Bindings crates (`bindings_ffi`, `bindings_node`, `bindings_wasm`) — inspect for FFI-visible group_id fields. Likely these stay `Vec<u8>` at the FFI boundary, with conversion inside the binding implementation. Each step compiles green on its own. Step boundaries are the natural commit boundaries. ## Error Handling No new error paths in phase 1. Conversions are all infallible. Phase 2 introduces `GroupIdError::InvalidLength` when the 16-byte constraint lands. ## Testing - Existing `group_id` unit tests at `crates/xmtp_proto/src/types/ids/group_id.rs` stay green. - Add unit tests for `OpenMlsGroupIdExt::to_xmtp` and `GroupId::to_openmls` round-trips. - Integration coverage: existing `just test v3` and `just test d14n` suites must pass. The migration is a type refactor; behavioral tests act as regression guards. - Per `CLAUDE.md`: `just lint` before commit. For bindings changes, also `just node lint`. ## Risks & Mitigations - **Risk:** migrating 236 sites in one change produces a massive diff. - **Mitigation:** bottom-up per-crate commits. Each commit self-compiles and self-tests. Reviewer reads one crate at a time. - **Risk:** phase 2 `[u8; 16]` swap gets blocked by a `Deref<Bytes>` or `Borrow<[u8]>` consumer added during phase 1. - **Mitigation:** code review checklist item — new call sites must use `AsRef<[u8]>` / `.as_slice()` only, not `Deref<Bytes>`. Existing `Deref` uses flagged for rewrite during their file's migration step. - **Risk:** openmls name collision causes confusion where imports are implicit. - **Mitigation:** the `as OpenMlsGroupId` rename is the single project-wide convention, documented in this spec and applied in every file that crosses the boundary. - **Risk:** FFI boundary inadvertently exposes `GroupId` via uniffi/napi generated code, breaking mobile/node consumers. - **Mitigation:** bindings crates keep `Vec<u8>` at the FFI surface. Conversion wraps into `GroupId` inside the binding implementation only. ## Phase 2 Preview (out of scope, context only) - Swap inner repr from `bytes::Bytes` to `[u8; 16]`. - Add `Copy` impl. - Replace `From<Vec<u8>>` / `From<&[u8]>` with `TryFrom<&[u8]>` returning `GroupIdError::InvalidLength`. - Remove `Deref<Bytes>`. - Add Diesel `ToSql`/`FromSql` impls and swap Diesel model fields to `GroupId`. - Tighten `FromStr` to require 32 hex chars.
4 条评论