1use prost::Message;
3use std::collections::HashMap;
4use xmtp_proto::{
5 ConversionError, backend_v1 as wire,
6 types::{self, IncomingBatchLimits, OrderedEnvelopeBatch, Topic, TopicCursor, TopicKind},
7};
8
9#[derive(Debug, thiserror::Error)]
11pub enum EnvelopeError {
12 #[error(transparent)]
14 Conversion(#[from] ConversionError),
15 #[error(transparent)]
17 Validation(#[from] xmtp_mls_validation::ValidationError),
18 #[error("incoming delivery exceeds its row or byte limit")]
20 Capacity,
21}
22impl xmtp_common::RetryableError for EnvelopeError {
23 fn is_retryable(&self) -> bool {
24 false
25 }
26}
27impl From<EnvelopeError> for xmtp_proto::api::ApiClientError {
28 fn from(error: EnvelopeError) -> Self {
29 Self::other(error)
30 }
31}
32
33fn invalid(item: &'static str) -> ConversionError {
34 ConversionError::InvalidValue {
35 item,
36 expected: "valid backend envelope",
37 got: "missing or invalid field".into(),
38 }
39}
40
41pub fn ordered_batches(
46 cursors: &mut TopicCursor,
47 envelopes: Vec<wire::ServerEnvelope>,
48 limits: IncomingBatchLimits,
49) -> Result<Vec<OrderedEnvelopeBatch>, EnvelopeError> {
50 let bytes = envelopes.iter().try_fold(0usize, |bytes, envelope| {
51 bytes.checked_add(envelope.encoded_len())
52 });
53 if envelopes.len() > limits.max_rows || bytes.is_none_or(|bytes| bytes > limits.max_bytes) {
54 return Err(EnvelopeError::Capacity);
55 }
56 let mut next = cursors.clone();
57 let mut batches: Vec<OrderedEnvelopeBatch> = Vec::new();
58 let mut indices = HashMap::new();
59 for envelope in envelopes {
60 let meta = envelope.meta.as_ref().ok_or_else(|| invalid("metadata"))?;
61 let topic = Topic::parse(&meta.topic.as_ref().ok_or_else(|| invalid("topic"))?.topic)?;
62 let (_, sequence, _) = metadata(meta, topic.kind())?;
63 let cursor = next
64 .get_mut(&topic)
65 .ok_or_else(|| invalid("unrequested topic"))?;
66 if sequence <= *cursor {
67 return Err(invalid("ordered cursor").into());
68 }
69 let index = *indices.entry(topic.clone()).or_insert_with(|| {
70 let index = batches.len();
71 batches.push(OrderedEnvelopeBatch {
72 topic,
73 after: *cursor,
74 envelopes: Vec::new(),
75 });
76 index
77 });
78 batches[index].envelopes.push(envelope);
79 *cursor = sequence;
80 }
81 *cursors = next;
82 Ok(batches)
83}
84
85pub fn registration_targets(
88 starts: &TopicCursor,
89 targets: Vec<wire::CatchupTarget>,
90) -> Result<TopicCursor, EnvelopeError> {
91 if targets.len() != starts.len() {
92 return Err(invalid("registration target count").into());
93 }
94 let mut output = TopicCursor::new();
95 for target in targets {
96 let topic = Topic::parse(&target.topic.ok_or_else(|| invalid("target topic"))?.topic)?;
97 if !starts.contains_key(&topic)
98 || target.through_sequence_id > i64::MAX as u64
99 || output
100 .insert(topic, types::Cursor(target.through_sequence_id))
101 .is_some()
102 {
103 return Err(invalid("registration target").into());
104 }
105 }
106 Ok(output)
107}
108
109pub fn metadata(
111 meta: &wire::EnvelopeMeta,
112 kind: TopicKind,
113) -> Result<(Topic, types::Cursor, chrono::DateTime<chrono::Utc>), ConversionError> {
114 let topic = Topic::parse(&meta.topic.as_ref().ok_or_else(|| invalid("topic"))?.topic)?;
115 if topic.kind() != kind {
116 return Err(invalid("topic kind"));
117 }
118 let seq = meta
119 .cursor
120 .as_ref()
121 .ok_or_else(|| invalid("cursor"))?
122 .sequence_id;
123 if seq == 0 || seq > i64::MAX as u64 {
124 return Err(invalid("sequence id"));
125 }
126 let timestamp = i64::try_from(meta.server_ns).map_err(|_| invalid("server timestamp"))?;
127 message_hash(meta)?;
128 Ok((
129 topic,
130 types::Cursor(seq),
131 chrono::DateTime::from_timestamp_nanos(timestamp),
132 ))
133}
134
135pub fn message_hash(meta: &wire::EnvelopeMeta) -> Result<Vec<u8>, ConversionError> {
137 match meta.message_hash.as_ref().and_then(|h| h.hash.as_ref()) {
138 Some(wire::message_hash::Hash::Sha256(bytes)) if bytes.len() == 32 => Ok(bytes.clone()),
139 _ => Err(invalid("message hash")),
140 }
141}
142
143fn parts(
144 envelope: wire::ServerEnvelope,
145 kind: TopicKind,
146) -> Result<(wire::EnvelopeMeta, wire::client_envelope::Payload), EnvelopeError> {
147 let meta = envelope.meta.ok_or_else(|| invalid("metadata"))?;
148 metadata(&meta, kind)?;
149 let payload = envelope
150 .envelope
151 .and_then(|e| e.payload)
152 .ok_or_else(|| invalid("payload"))?;
153 Ok((meta, payload))
154}
155
156pub fn decode_group_message(
158 envelope: wire::ServerEnvelope,
159) -> Result<types::GroupMessage, EnvelopeError> {
160 let (meta, payload) = parts(envelope, TopicKind::GroupMessagesV1)?;
161 let (topic, cursor, created_ns) = metadata(&meta, TopicKind::GroupMessagesV1)?;
162 let wire::client_envelope::Payload::GroupMessage(group) = payload else {
163 return Err(invalid("group payload").into());
164 };
165 let message = xmtp_mls_validation::parse_group_message(&group.data)?;
166 if message.group_id().as_slice() != topic.identifier() {
167 return Err(invalid("group id").into());
168 }
169 Ok(types::GroupMessage {
170 cursor,
171 created_ns,
172 group_id: topic.identifier().try_into()?,
173 message,
174 payload_hash: xmtp_common::sha256_array(&group.data).to_vec(),
175 sender_hmac: group.sender_hmac,
176 should_push: group.should_push,
177 envelope_hash: Some(message_hash(&meta)?),
178 expiry_ns: Some(meta.expiry_ns),
179 })
180}
181
182pub fn decode_group_message_metadata(
184 meta: wire::EnvelopeMeta,
185) -> Result<types::GroupMessageMetadata, EnvelopeError> {
186 let (topic, cursor, created_ns) = metadata(&meta, TopicKind::GroupMessagesV1)?;
187 Ok(types::GroupMessageMetadata {
188 cursor,
189 created_ns,
190 group_id: topic.identifier().try_into()?,
191 envelope_hash: Some(message_hash(&meta)?),
192 expiry_ns: Some(meta.expiry_ns),
193 })
194}
195
196pub fn decode_welcome_message(
198 envelope: wire::ServerEnvelope,
199) -> Result<types::WelcomeMessage, EnvelopeError> {
200 let (meta, payload) = parts(envelope, TopicKind::WelcomeMessagesV1)?;
201 let (topic, cursor, created_ns) = metadata(&meta, TopicKind::WelcomeMessagesV1)?;
202 let wire::client_envelope::Payload::WelcomeMessage(welcome) = payload else {
203 return Err(invalid("welcome payload").into());
204 };
205 let variant = match welcome.version.ok_or_else(|| invalid("welcome version"))? {
206 wire::welcome_message::Version::V1(v) => {
207 if v.installation_key != topic.identifier() {
208 return Err(invalid("installation id").into());
209 }
210 types::WelcomeMessageV1 {
211 installation_key: v.installation_key.try_into()?,
212 hpke_public_key: v.hpke_public_key,
213 wrapper_algorithm: v
214 .wrapper_algorithm
215 .try_into()
216 .map_err(ConversionError::from)?,
217 data: v.data,
218 welcome_metadata: v.welcome_metadata,
219 }
220 .into()
221 }
222 wire::welcome_message::Version::WelcomePointer(v) => {
223 if v.installation_key != topic.identifier() {
224 return Err(invalid("installation id").into());
225 }
226 types::WelcomePointer {
227 installation_key: v.installation_key.try_into()?,
228 hpke_public_key: v.hpke_public_key,
229 wrapper_algorithm: v
230 .wrapper_algorithm
231 .try_into()
232 .map_err(ConversionError::from)?,
233 welcome_pointer: v.welcome_pointer,
234 }
235 .into()
236 }
237 };
238 Ok(types::WelcomeMessage {
239 cursor,
240 created_ns,
241 variant,
242 })
243}
244
245pub fn decode_key_package(
247 envelope: wire::ServerEnvelope,
248) -> Result<wire::KeyPackage, EnvelopeError> {
249 let (_, payload) = parts(envelope, TopicKind::KeyPackagesV1)?;
250 match payload {
251 wire::client_envelope::Payload::KeyPackage(key) => Ok(key),
252 _ => Err(invalid("key package payload").into()),
253 }
254}
255
256pub fn decode_commit_log_entry(
258 envelope: wire::ServerEnvelope,
259) -> Result<types::CommitLogEntry, EnvelopeError> {
260 let (meta, payload) = parts(envelope, TopicKind::CommitLogEntriesV1)?;
261 let wire::client_envelope::Payload::CommitLogEntry(payload) = payload else {
262 return Err(invalid("commit log payload").into());
263 };
264 let entry =
265 xmtp_mls_common::commit_log::decode_commit_log(&payload.serialized_commit_log_entry)
266 .map_err(ConversionError::from)?;
267 let (topic, _, _) = metadata(&meta, TopicKind::CommitLogEntriesV1)?;
268 if entry.group_id != topic.identifier() {
269 return Err(invalid("commit log group id").into());
270 }
271 Ok(types::CommitLogEntry {
272 meta,
273 entry,
274 payload,
275 })
276}
277
278pub fn decode_identity_update(
280 envelope: wire::ServerEnvelope,
281) -> Result<types::IdentityUpdateLog, EnvelopeError> {
282 let (meta, payload) = parts(envelope, TopicKind::IdentityUpdatesV1)?;
283 let wire::client_envelope::Payload::IdentityUpdate(update) = payload else {
284 return Err(invalid("identity payload").into());
285 };
286 let (topic, _, _) = metadata(&meta, TopicKind::IdentityUpdatesV1)?;
287 if hex::decode(&update.inbox_id).map_err(|_| invalid("inbox id"))? != topic.identifier() {
288 return Err(invalid("inbox id").into());
289 }
290 Ok(types::IdentityUpdateLog { meta, update })
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn envelope(topic: &Topic, sequence: u64) -> wire::ServerEnvelope {
298 wire::ServerEnvelope {
299 meta: Some(wire::EnvelopeMeta {
300 topic: Some(wire::Topic {
301 topic: topic.cloned_vec(),
302 }),
303 cursor: Some(wire::Cursor {
304 sequence_id: sequence,
305 }),
306 message_hash: Some(wire::MessageHash {
307 hash: Some(wire::message_hash::Hash::Sha256(vec![7; 32])),
308 }),
309 ..Default::default()
310 }),
311 envelope: Some(wire::ClientEnvelope {
313 payload: Some(wire::client_envelope::Payload::GroupMessage(
314 wire::GroupMessage {
315 data: vec![255],
316 ..Default::default()
317 },
318 )),
319 }),
320 }
321 }
322
323 #[xmtp_common::test(unwrap_try = true)]
325 fn ordered_batches_keep_sparse_positions_and_authoritative_bytes() {
326 let a = Topic::new_group_message([1; 16]);
327 let b = Topic::new_group_message([2; 16]);
328 let mut cursors = [(a.clone(), types::Cursor(2)), (b.clone(), types::Cursor(0))].into();
329 let original = envelope(&a, 8);
330 let batches = ordered_batches(
331 &mut cursors,
332 vec![original.clone(), envelope(&b, 11), envelope(&a, 20)],
333 IncomingBatchLimits {
334 max_rows: 3,
335 max_bytes: 4096,
336 },
337 )?;
338 assert_eq!(batches.len(), 2);
339 assert_eq!(batches[0].after, types::Cursor(2));
340 assert_eq!(batches[0].envelopes[0], original);
341 assert_eq!(batches[1].after, types::Cursor(0));
342 assert_eq!(cursors[&a], types::Cursor(20));
343 assert_eq!(cursors[&b], types::Cursor(11));
344 }
345
346 #[xmtp_common::test(unwrap_try = true)]
348 fn failed_frame_does_not_advance_any_topic() {
349 let a = Topic::new_group_message([1; 16]);
350 let b = Topic::new_group_message([2; 16]);
351 let starts: TopicCursor =
352 [(a.clone(), types::Cursor(2)), (b.clone(), types::Cursor(0))].into();
353 for envelopes in [
354 vec![envelope(&a, 8), envelope(&b, 0)],
355 vec![envelope(&a, 8), envelope(&a, 7)],
356 ] {
357 let mut cursors = starts.clone();
358 assert!(
359 ordered_batches(
360 &mut cursors,
361 envelopes,
362 IncomingBatchLimits {
363 max_rows: 2,
364 max_bytes: 4096
365 }
366 )
367 .is_err()
368 );
369 assert_eq!(cursors, starts);
370 }
371 let mut cursors = starts.clone();
372 assert!(matches!(
373 ordered_batches(
374 &mut cursors,
375 vec![envelope(&a, 8)],
376 IncomingBatchLimits {
377 max_rows: 1,
378 max_bytes: 1
379 }
380 ),
381 Err(EnvelopeError::Capacity)
382 ));
383 assert_eq!(cursors, starts);
384 }
385}