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 {
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 CommitLogEntry, GroupMessage, KeyPackage, WelcomeMessage,
32 client_envelope::Payload,
33 welcome_message::{V1, Version},
34 },
35 identity::associations::{IdentityUpdate, RecoverableEd25519Signature},
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 }))
52 );
53
54 let cases = [
55 (
56 group_envelope.clone(),
57 "0a0b0a0300ff42120201021801",
58 "5fd2916dcdd5fe79bb09ab21d545d5637307ace6e5352d4c2d287dbdd9338888",
59 ),
60 (
61 ClientEnvelope {
62 payload: Some(Payload::WelcomeMessage(WelcomeMessage {
63 version: Some(Version::V1(V1 {
64 installation_key: vec![0x10, 0x11],
65 data: vec![0x12],
66 hpke_public_key: vec![0x13, 0x14],
67 wrapper_algorithm: 1,
68 welcome_metadata: vec![0x15],
69 })),
70 })),
71 },
72 "12120a100a0210111201121a02131420012a0115",
73 "9a47fe38765eeebb1f272663381503c987fabbf9bb99b023fc88eaaffda60d5f",
74 ),
75 (
76 ClientEnvelope {
77 payload: Some(Payload::KeyPackage(KeyPackage {
78 key_package_tls_serialized: vec![0x20, 0x21, 0x22],
79 })),
80 },
81 "1a050a03202122",
82 "3f46232f37d76bcc2e80d3c54c9f9d5a7273d78ef8f91336249ef79746fd06dc",
83 ),
84 (
85 ClientEnvelope {
86 payload: Some(Payload::IdentityUpdate(IdentityUpdate {
87 client_timestamp_ns: 7,
88 inbox_id: "ab".into(),
89 ..Default::default()
90 })),
91 },
92 "220610071a026162",
93 "0aab40455ce3dacc36006ef421e2746016960606e24d125946e7a73dc9cf6f9b",
94 ),
95 (
96 ClientEnvelope {
97 payload: Some(Payload::CommitLogEntry(CommitLogEntry {
98 serialized_commit_log_entry: vec![0x30, 0x31],
99 signature: Some(RecoverableEd25519Signature {
100 bytes: vec![0x32],
101 public_key: vec![0x33, 0x34],
102 }),
103 })),
104 },
105 "2a0d0a02303112070a013212023334",
106 "108bc3029f48dda98d9d43238154830ae0ee71e7d2e083264a2b07a3d736bee2",
107 ),
108 ];
109
110 for (envelope, expected_bytes, expected_hash) in cases {
111 let canonical = canonical_envelope(&envelope);
112 assert_eq!(hex::encode(&canonical.bytes), expected_bytes);
113 assert_eq!(hex::encode(canonical.hash), expected_hash);
114 let decoded = ClientEnvelope::decode(canonical.bytes.as_slice())?;
115 assert_eq!(decoded, envelope);
116 assert_eq!(canonical_envelope(&decoded), canonical);
117 }
118
119 let Some(Payload::GroupMessage(group)) = group_envelope.payload else {
120 unreachable!("first golden vector is a group message")
121 };
122 assert_eq!(
123 hex::encode(sha256_array(&group.data)),
124 "f803bec586282caafe409609aae90eb09f6d4cddb6e04431ddf76d22e7dcacd6"
125 );
126 assert_ne!(
127 canonical_envelope(&ClientEnvelope {
128 payload: Some(Payload::GroupMessage(group.clone())),
129 })
130 .hash,
131 sha256_array(&group.data)
132 );
133 }
134}