1use crate::context::CoreContext;
4use crate::worker::{BoxedWorker, NewWorker, Worker, WorkerFactory, WorkerKind, WorkerResult};
5use std::sync::{
6 Arc,
7 atomic::{AtomicI64, Ordering},
8};
9use xmtp_common::time::{self, Duration};
10use xmtp_events::{ClientEvent, EventWriter, HmacKeysUpdated};
11
12pub fn hmac_epoch() -> i64 {
14 xmtp_push_types::hmac_epoch(xmtp_common::time::now_secs())
15}
16
17pub struct HmacEpochWorker<Context> {
18 context: Context,
19 last_epoch: Arc<AtomicI64>,
20 #[cfg(test)]
21 clock: Option<TestClock>,
22}
23
24#[cfg(test)]
25struct TestClock {
26 now: Arc<AtomicI64>,
27 ticks: futures::channel::mpsc::UnboundedReceiver<i64>,
28 waits: futures::channel::mpsc::UnboundedSender<Duration>,
29}
30
31impl<Context> HmacEpochWorker<Context> {
32 fn now(&self) -> i64 {
33 #[cfg(test)]
34 if let Some(clock) = &self.clock {
35 return clock.now.load(Ordering::Acquire);
36 }
37 time::now_secs()
38 }
39
40 async fn wait(&mut self, duration: Duration) {
41 #[cfg(test)]
42 if let Some(clock) = &mut self.clock {
43 use futures::StreamExt;
44 clock
45 .waits
46 .unbounded_send(duration)
47 .expect("test wait observer must stay open");
48 let now = clock.ticks.next().await.expect("test clock must stay open");
49 clock.now.store(now, Ordering::Release);
50 return;
51 }
52 time::sleep(duration).await;
53 }
54
55 fn wait_until_next_epoch(&self, now: i64) -> Duration {
56 let next =
57 (self.last_epoch.load(Ordering::Acquire) + 1) * xmtp_push_types::HMAC_EPOCH_SECONDS;
58 Duration::from_secs((next - now).max(1) as u64)
59 }
60
61 fn observe_epoch(&mut self, current: i64) -> bool {
62 loop {
63 let previous = self.last_epoch.load(Ordering::Acquire);
64 if current <= previous {
65 return false;
66 }
67 if self
68 .last_epoch
69 .compare_exchange(previous, current, Ordering::AcqRel, Ordering::Acquire)
70 .is_ok()
71 {
72 return true;
73 }
74 }
75 }
76}
77
78impl<Context: CoreContext> HmacEpochWorker<Context> {
79 fn report_epoch(&mut self, observed: i64) {
81 if self.observe_epoch(observed) {
82 self.context.task_channels().mark_notification_changed();
83 self.context
84 .events()
85 .emit(Some(ClientEvent::HmacKeysUpdated(HmacKeysUpdated)), None);
86 }
87 }
88}
89
90struct Factory<Context> {
91 context: Context,
92 last_epoch: Arc<AtomicI64>,
93}
94
95impl<Context> Factory<Context> {
96 fn new(context: Context, startup_epoch: i64) -> Self {
97 Self {
98 context,
99 last_epoch: Arc::new(AtomicI64::new(startup_epoch)),
100 }
101 }
102}
103
104impl<Context: CoreContext + 'static> WorkerFactory for Factory<Context> {
105 fn kind(&self) -> WorkerKind {
106 WorkerKind::HmacEpoch
107 }
108
109 fn create(&self) -> BoxedWorker {
110 Box::new(HmacEpochWorker {
111 context: self.context.clone(),
112 last_epoch: self.last_epoch.clone(),
113 #[cfg(test)]
114 clock: None,
115 })
116 }
117}
118
119impl<Context: CoreContext + 'static> NewWorker<Context> for HmacEpochWorker<Context> {
120 fn factory(context: Context) -> impl WorkerFactory + 'static {
121 Factory::new(context, hmac_epoch())
122 }
123}
124
125#[xmtp_common::async_trait]
126impl<Context: CoreContext + 'static> Worker for HmacEpochWorker<Context> {
127 fn kind(&self) -> WorkerKind {
128 WorkerKind::HmacEpoch
129 }
130
131 async fn run_tasks(&mut self) -> WorkerResult<()> {
132 loop {
133 let now = self.now();
134 self.wait(self.wait_until_next_epoch(now)).await;
135 self.report_epoch(self.now() / xmtp_push_types::HMAC_EPOCH_SECONDS);
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::test_context::real_context;
144 use xmtp_events::{EventFilter, EventKind};
145 use xmtp_proto::api::HasStats;
146
147 #[xmtp_common::test(unwrap_try = true)]
148 async fn reports_only_new_epochs_after_startup() {
149 let mut worker = HmacEpochWorker {
150 context: (),
151 last_epoch: Arc::new(AtomicI64::new(10)),
152 clock: None,
153 };
154 assert!(!worker.observe_epoch(10));
155 assert!(worker.observe_epoch(11));
156 assert!(!worker.observe_epoch(11));
157 assert!(worker.observe_epoch(12));
158 }
159
160 #[xmtp_common::test(unwrap_try = true)]
162 async fn epoch_timer_turn_emits_once_and_invalidates_notifications() {
163 let context = real_context().await;
164 let current = hmac_epoch();
165 let mut worker = HmacEpochWorker {
166 context: context.clone(),
167 last_epoch: Arc::new(AtomicI64::new(current)),
168 clock: None,
169 };
170 let events = context
171 .events()
172 .subscribe_app(EventFilter::new([EventKind::HmacKeysUpdated]))?;
173 let boundary = (current + 1) * xmtp_push_types::HMAC_EPOCH_SECONDS;
174 assert_eq!(
175 worker.wait_until_next_epoch(boundary - 1),
176 Duration::from_secs(1)
177 );
178 let previous_revision = context.task_channels().notification_revision();
179 worker.report_epoch(current);
180 assert!(events.drain().is_empty());
181 worker.report_epoch(current + 1);
182 assert!(matches!(
183 events.drain().as_slice(),
184 [xmtp_events::EventEnvelope {
185 client: Some(ClientEvent::HmacKeysUpdated(_)),
186 ..
187 }]
188 ));
189 assert_eq!(
190 context.task_channels().notification_revision(),
191 previous_revision + 1
192 );
193 worker.report_epoch(current + 1);
194 assert!(events.drain().is_empty());
195 }
196 #[xmtp_common::test(unwrap_try = true)]
198 async fn running_epoch_worker_skips_closed_epochs_and_uses_no_network() {
199 let context = real_context().await;
200 let events = context
201 .events()
202 .subscribe_app(EventFilter::new([EventKind::HmacKeysUpdated]))?;
203 let stats = context.api().api_client.mls_stats();
204 let identity = context.api().api_client.identity_stats();
205 let counts = || {
206 [
207 stats.publish.get_count(),
208 stats.create_upload.get_count(),
209 stats.query.get_count(),
210 stats.query_newest.get_count(),
211 stats.subscribe.get_count(),
212 stats.subscribe_static.get_count(),
213 identity.get_inbox_ids.get_count(),
214 identity.verify_smart_contract_wallet_signatures.get_count(),
215 ]
216 };
217 let before = counts();
218 let revision = context.task_channels().notification_revision();
219 let width = xmtp_push_types::HMAC_EPOCH_SECONDS;
220 let now = Arc::new(AtomicI64::new(10 * width));
221 let (ticks, receiver) = futures::channel::mpsc::unbounded();
222 let (waits, mut scheduled) = futures::channel::mpsc::unbounded();
223 let factory = Factory::new(context.clone(), 10);
224 let mut worker = HmacEpochWorker {
225 context: factory.context.clone(),
226 last_epoch: factory.last_epoch.clone(),
227 clock: Some(TestClock {
228 now: now.clone(),
229 ticks: receiver,
230 waits,
231 }),
232 };
233 let mut running = Box::pin(worker.run_tasks());
234 assert!(futures::poll!(&mut running).is_pending());
235 assert!(events.drain().is_empty());
236 assert_eq!(scheduled.try_recv()?, Duration::from_secs(width as u64));
237 let mut previous = 10;
238 for (epoch, expected) in [(10, 0), (11, 1), (11, 0), (14, 1), (13, 0), (14, 0)] {
239 ticks.unbounded_send(epoch * width)?;
240 assert!(futures::poll!(&mut running).is_pending());
241 let emitted = events.drain();
242 assert_eq!(emitted.len(), expected);
243 previous = previous.max(epoch);
244 assert_eq!(
245 scheduled.try_recv()?,
246 Duration::from_secs(((previous + 1 - epoch) * width) as u64)
247 );
248 assert!(
249 emitted
250 .iter()
251 .all(|event| matches!(event.client, Some(ClientEvent::HmacKeysUpdated(_))))
252 );
253 }
254 drop(running);
255 assert_eq!(
256 context.task_channels().notification_revision(),
257 revision + 2
258 );
259 now.store(18 * width, Ordering::Release);
260 let (ticks, receiver) = futures::channel::mpsc::unbounded();
261 let (waits, mut scheduled) = futures::channel::mpsc::unbounded();
262 let factory = Factory::new(context.clone(), 18);
263 let mut reopened = HmacEpochWorker {
264 context: factory.context.clone(),
265 last_epoch: factory.last_epoch.clone(),
266 clock: Some(TestClock {
267 now,
268 ticks: receiver,
269 waits,
270 }),
271 };
272 let mut running = Box::pin(reopened.run_tasks());
273 assert!(futures::poll!(&mut running).is_pending());
274 assert_eq!(scheduled.try_recv()?, Duration::from_secs(width as u64));
275 ticks.unbounded_send(18 * width)?;
276 assert!(futures::poll!(&mut running).is_pending());
277 assert!(events.drain().is_empty());
278 ticks.unbounded_send(19 * width)?;
279 assert!(futures::poll!(&mut running).is_pending());
280 assert_eq!(events.drain().len(), 1);
281 assert_eq!(
282 context.task_channels().notification_revision(),
283 revision + 3
284 );
285 assert_eq!(counts(), before);
286 }
287}