1use 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 {
21 let bytes = envelope.encode_to_vec();
22 let hash = sha256_array(&bytes);
23 CanonicalEnvelope { bytes, hash }
24}
25
26#[cfg(test)]
27mod tests {
28 use super::*;
29 use crate::xmtp::{
30 backend::v1::{
31 GroupMessage, KeyPackage, SealedCommitLogEntry, WelcomeMessage,
32 client_envelope::Payload,
33 welcome_message::{V1, Version},
34 },
35 identity::associations::IdentityUpdate,
36 };
37
38 #[xmtp_common::test(unwrap_try = true)]
41 fn all_payloads_have_stable_canonical_bytes_and_distinct_outer_hashes() {
42 let input = hex::decode("0a0b1801120201020a0300ff42800100")?;
44 let group_envelope = ClientEnvelope::decode(input.as_slice())?;
45 assert_eq!(
46 group_envelope.payload,
47 Some(Payload::GroupMessage(GroupMessage {
48 data: vec![0, 255, 66],
49 sender_hmac: vec![1, 2],
50 should_push: true,
51 signature: vec![],
52 }))
53 );
54
55 let cases = [
56 (
57 group_envelope.clone(),
58 "0a0b0a0300ff42120201021801",
59 "5fd2916dcdd5fe79bb09ab21d545d5637307ace6e5352d4c2d287dbdd9338888",
60 ),
61 (
62 ClientEnvelope {
63 payload: Some(Payload::WelcomeMessage(WelcomeMessage {
64 version: Some(Version::V1(V1 {
65 installation_key: vec![0x10, 0x11],
66 data: vec![0x12],
67 hpke_public_key: vec![0x13, 0x14],
68 wrapper_algorithm: 1,
69 welcome_metadata: vec![0x15],
70 })),
71 })),
72 },
73 "12120a100a0210111201121a02131420012a0115",
74 "9a47fe38765eeebb1f272663381503c987fabbf9bb99b023fc88eaaffda60d5f",
75 ),
76 (
77 ClientEnvelope {
78 payload: Some(Payload::KeyPackage(KeyPackage {
79 key_package_tls_serialized: vec![0x20, 0x21, 0x22],
80 })),
81 },
82 "1a050a03202122",
83 "3f46232f37d76bcc2e80d3c54c9f9d5a7273d78ef8f91336249ef79746fd06dc",
84 ),
85 (
86 ClientEnvelope {
87 payload: Some(Payload::IdentityUpdate(IdentityUpdate {
88 client_timestamp_ns: 7,
89 inbox_id: "ab".into(),
90 ..Default::default()
91 })),
92 },
93 "220610071a026162",
94 "0aab40455ce3dacc36006ef421e2746016960606e24d125946e7a73dc9cf6f9b",
95 ),
96 (
98 ClientEnvelope {
99 payload: Some(Payload::GroupMessage(GroupMessage {
100 data: vec![0, 255, 66],
101 sender_hmac: vec![1, 2],
102 should_push: true,
103 signature: vec![0x41, 0x42],
104 })),
105 },
106 "0a0f0a0300ff4212020102180122024142",
107 "4b88066b5fe9b4de9d603021b078abe4b19255dddfce9d074237a717a2c7854d",
108 ),
109 (
111 ClientEnvelope {
112 payload: Some(Payload::SealedCommitLogEntry(SealedCommitLogEntry {
113 commit_log_id: vec![0x50, 0x51],
114 nonce: vec![0x52],
115 ciphertext: vec![0x53, 0x54],
116 signature: vec![0x55],
117 })),
118 },
119 "320e0a0250511201521a025354220155",
120 "a3d80629f11646414c6170dfe64f675b4d2328a23082169379fdd1b28b5581bf",
121 ),
122 ];
123
124 for (envelope, expected_bytes, expected_hash) in cases {
125 let canonical = canonical_envelope(&envelope);
126 assert_eq!(hex::encode(&canonical.bytes), expected_bytes);
127 assert_eq!(hex::encode(canonical.hash), expected_hash);
128 let decoded = ClientEnvelope::decode(canonical.bytes.as_slice())?;
129 assert_eq!(decoded, envelope);
130 assert_eq!(canonical_envelope(&decoded), canonical);
131 }
132
133 let retired =
136 ClientEnvelope::decode(hex::decode("2a0d0a02303112070a013212023334")?.as_slice())?;
137 assert_eq!(retired.payload, None);
138 assert!(canonical_envelope(&retired).bytes.is_empty());
139
140 let Some(Payload::GroupMessage(group)) = group_envelope.payload else {
141 unreachable!("first golden vector is a group message")
142 };
143 assert_eq!(
144 hex::encode(sha256_array(&group.data)),
145 "f803bec586282caafe409609aae90eb09f6d4cddb6e04431ddf76d22e7dcacd6"
146 );
147 assert_ne!(
148 canonical_envelope(&ClientEnvelope {
149 payload: Some(Payload::GroupMessage(group.clone())),
150 })
151 .hash,
152 sha256_array(&group.data)
153 );
154 }
155}