xmtp_proto/types/
backend_envelope.rs1use prost::Message;
2use xmtp_common::sha256_array;
3
4use crate::xmtp::backend::v1::ClientEnvelope;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CanonicalEnvelope {
9 pub bytes: Vec<u8>,
11 pub hash: [u8; 32],
13}
14
15pub fn canonical_envelope(envelope: &ClientEnvelope) -> CanonicalEnvelope {
20 let bytes = envelope.encode_to_vec();
21 let hash = sha256_array(&bytes);
22 CanonicalEnvelope { bytes, hash }
23}
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28 use crate::xmtp::{
29 backend::v1::{
30 CommitLogEntry, GroupMessage, KeyPackage, WelcomeMessage,
31 client_envelope::Payload,
32 welcome_message::{V1, Version},
33 },
34 identity::associations::{IdentityUpdate, RecoverableEd25519Signature},
35 };
36
37 #[xmtp_common::test(unwrap_try = true)]
39 fn all_payloads_have_stable_canonical_bytes_and_distinct_outer_hashes() {
40 let input = hex::decode("0a0b1801120201020a0300ff42800100")?;
42 let group_envelope = ClientEnvelope::decode(input.as_slice())?;
43 assert_eq!(
44 group_envelope.payload,
45 Some(Payload::GroupMessage(GroupMessage {
46 data: vec![0, 255, 66],
47 sender_hmac: vec![1, 2],
48 should_push: true,
49 }))
50 );
51
52 let cases = [
53 (
54 group_envelope.clone(),
55 "0a0b0a0300ff42120201021801",
56 "5fd2916dcdd5fe79bb09ab21d545d5637307ace6e5352d4c2d287dbdd9338888",
57 ),
58 (
59 ClientEnvelope {
60 payload: Some(Payload::WelcomeMessage(WelcomeMessage {
61 version: Some(Version::V1(V1 {
62 installation_key: vec![0x10, 0x11],
63 data: vec![0x12],
64 hpke_public_key: vec![0x13, 0x14],
65 wrapper_algorithm: 1,
66 welcome_metadata: vec![0x15],
67 })),
68 })),
69 },
70 "12120a100a0210111201121a02131420012a0115",
71 "9a47fe38765eeebb1f272663381503c987fabbf9bb99b023fc88eaaffda60d5f",
72 ),
73 (
74 ClientEnvelope {
75 payload: Some(Payload::KeyPackage(KeyPackage {
76 key_package_tls_serialized: vec![0x20, 0x21, 0x22],
77 })),
78 },
79 "1a050a03202122",
80 "3f46232f37d76bcc2e80d3c54c9f9d5a7273d78ef8f91336249ef79746fd06dc",
81 ),
82 (
83 ClientEnvelope {
84 payload: Some(Payload::IdentityUpdate(IdentityUpdate {
85 client_timestamp_ns: 7,
86 inbox_id: "ab".into(),
87 ..Default::default()
88 })),
89 },
90 "220610071a026162",
91 "0aab40455ce3dacc36006ef421e2746016960606e24d125946e7a73dc9cf6f9b",
92 ),
93 (
94 ClientEnvelope {
95 payload: Some(Payload::CommitLogEntry(CommitLogEntry {
96 serialized_commit_log_entry: vec![0x30, 0x31],
97 signature: Some(RecoverableEd25519Signature {
98 bytes: vec![0x32],
99 public_key: vec![0x33, 0x34],
100 }),
101 })),
102 },
103 "2a0d0a02303112070a013212023334",
104 "108bc3029f48dda98d9d43238154830ae0ee71e7d2e083264a2b07a3d736bee2",
105 ),
106 ];
107
108 for (envelope, expected_bytes, expected_hash) in cases {
109 let canonical = canonical_envelope(&envelope);
110 assert_eq!(hex::encode(&canonical.bytes), expected_bytes);
111 assert_eq!(hex::encode(canonical.hash), expected_hash);
112 let decoded = ClientEnvelope::decode(canonical.bytes.as_slice())?;
113 assert_eq!(decoded, envelope);
114 assert_eq!(canonical_envelope(&decoded), canonical);
115 }
116
117 let Some(Payload::GroupMessage(group)) = group_envelope.payload else {
118 unreachable!("first golden vector is a group message")
119 };
120 assert_eq!(
121 hex::encode(sha256_array(&group.data)),
122 "f803bec586282caafe409609aae90eb09f6d4cddb6e04431ddf76d22e7dcacd6"
123 );
124 assert_ne!(
125 canonical_envelope(&ClientEnvelope {
126 payload: Some(Payload::GroupMessage(group.clone())),
127 })
128 .hash,
129 sha256_array(&group.data)
130 );
131 }
132}