xmtp_mls/worker/device_sync/
preference_sync.rs1use super::*;
2use xmtp_common::time::now_ns;
3use xmtp_db::consent_record::StoredConsentRecord;
4use xmtp_db::user_preferences::{HmacKey, StoredUserPreferences};
5use xmtp_proto::ConversionError;
6use xmtp_proto::xmtp::device_sync::content::HmacKeyUpdate as HmacKeyUpdateProto;
7use xmtp_proto::xmtp::device_sync::content::{
8 PreferenceUpdate as PreferenceUpdateProto, PreferenceUpdates,
9 device_sync_content::Content as ContentProto, preference_update::Update as UpdateProto,
10};
11
12#[derive(Clone, Debug, PartialEq)]
13pub enum PreferenceUpdate {
14 Consent(StoredConsentRecord),
15 Hmac { key: Vec<u8>, cycled_at_ns: i64 },
16}
17
18impl<Context> DeviceSyncClient<Context>
19where
20 Context: XmtpSharedContext,
21{
22 pub(crate) async fn sync_preferences(
23 &self,
24 updates: Vec<PreferenceUpdate>,
25 ) -> Result<Vec<PreferenceUpdate>, ClientError> {
26 self.send_device_sync_message(ContentProto::PreferenceUpdates(PreferenceUpdates {
27 updates: updates.clone().into_iter().map(From::from).collect(),
28 }))
29 .await?;
30
31 updates.iter().for_each(|update| match update {
32 PreferenceUpdate::Consent(_) => self.metrics.increment_metric(SyncMetric::ConsentSent),
33 PreferenceUpdate::Hmac { .. } => self.metrics.increment_metric(SyncMetric::HmacSent),
34 });
35
36 Ok(updates)
37 }
38
39 pub(crate) async fn cycle_hmac(&self) -> Result<(), ClientError> {
40 tracing::info!(
41 "[{}] Sending new HMAC key to sync group.",
42 self.context.installation_id()
43 );
44
45 self.sync_preferences(vec![PreferenceUpdate::Hmac {
46 key: HmacKey::random_key(),
47 cycled_at_ns: now_ns(),
48 }])
49 .await?;
50
51 Ok(())
52 }
53}
54
55pub(super) fn store_preference_updates(
56 updates: Vec<PreferenceUpdateProto>,
57 conn: &impl DbQuery,
58 handle: &WorkerMetrics<SyncMetric>,
59) -> Result<Vec<PreferenceUpdate>, StorageError> {
60 let mut changed = vec![];
61 for update in updates.into_iter().filter_map(|u| u.update) {
62 match update {
63 UpdateProto::Consent(consent_save) => {
64 tracing::info!(
65 "Storing consent update from sync group. State: {:?}",
66 consent_save.state
67 );
68
69 let consent_record: StoredConsentRecord = consent_save.try_into()?;
70 let updated = conn.insert_newer_consent_record(consent_record.clone())?;
71
72 if updated {
73 changed.push(PreferenceUpdate::Consent(consent_record));
74 }
75
76 handle.increment_metric(SyncMetric::ConsentReceived);
77 }
78 UpdateProto::Hmac(HmacKeyUpdateProto { key, cycled_at_ns }) => {
79 tracing::info!("Storing new HMAC key from sync group");
80 StoredUserPreferences::store_hmac_key(conn, &key, Some(cycled_at_ns))?;
81 changed.push(PreferenceUpdate::Hmac { key, cycled_at_ns });
82 handle.increment_metric(SyncMetric::HmacReceived);
83 }
84 }
85 }
86
87 Ok(changed)
88}
89
90impl TryFrom<PreferenceUpdateProto> for PreferenceUpdate {
91 type Error = ConversionError;
92 fn try_from(update: PreferenceUpdateProto) -> Result<Self, Self::Error> {
93 let Some(update) = update.update else {
94 return Err(ConversionError::Unspecified("update"));
95 };
96 update.try_into()
97 }
98}
99impl TryFrom<UpdateProto> for PreferenceUpdate {
100 type Error = ConversionError;
101 fn try_from(update: UpdateProto) -> Result<Self, Self::Error> {
102 let update = match update {
103 UpdateProto::Consent(consent) => Self::Consent(consent.try_into()?),
104 UpdateProto::Hmac(HmacKeyUpdateProto { key, cycled_at_ns }) => {
105 Self::Hmac { key, cycled_at_ns }
106 }
107 };
108 Ok(update)
109 }
110}
111
112impl From<PreferenceUpdate> for PreferenceUpdateProto {
113 fn from(update: PreferenceUpdate) -> Self {
114 PreferenceUpdateProto {
115 update: Some(match update {
116 PreferenceUpdate::Consent(consent) => UpdateProto::Consent(consent.into()),
117 PreferenceUpdate::Hmac { key, cycled_at_ns } => {
118 UpdateProto::Hmac(HmacKeyUpdateProto { key, cycled_at_ns })
119 }
120 }),
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use crate::{tester, worker::device_sync::worker::SyncMetric};
128 use xmtp_db::user_preferences::StoredUserPreferences;
129
130 #[rstest::rstest]
131 #[xmtp_common::test(unwrap_try = true)]
132 async fn test_hmac_sync() {
133 tester!(amal_a, sync_worker);
134 tester!(amal_b, from: amal_a);
135
136 amal_a.test_has_same_sync_group_as(&amal_b).await?;
137
138 amal_a
139 .worker()
140 .register_interest(SyncMetric::HmacSent, 1)
141 .wait()
142 .await?;
143
144 amal_a.sync_all_welcomes_and_device_sync_groups().await?;
145 amal_a
146 .worker()
147 .register_interest(SyncMetric::HmacReceived, 1)
148 .wait()
149 .await?;
150
151 amal_b
153 .context
154 .device_sync_client()
155 .get_sync_group()
156 .await?
157 .sync()
158 .await?;
159 amal_b
160 .worker()
161 .register_interest(SyncMetric::HmacReceived, 1)
162 .wait()
163 .await?;
164
165 let pref_a = StoredUserPreferences::load(amal_a.context.db())?;
166 let pref_b = StoredUserPreferences::load(amal_b.context.db())?;
167
168 assert_eq!(pref_a.hmac_key, pref_b.hmac_key);
169
170 amal_a
171 .identity_updates()
172 .revoke_installations(vec![amal_b.context.installation_id().to_vec()])
173 .await?;
174
175 amal_a.sync_all_welcomes_and_device_sync_groups().await?;
176 amal_a
177 .worker()
178 .register_interest(SyncMetric::HmacReceived, 2)
179 .wait()
180 .await?;
181 let new_pref_a = StoredUserPreferences::load(amal_a.context.db())?;
182 assert_ne!(pref_a.hmac_key, new_pref_a.hmac_key);
183 }
184}