Skip to main content

xmtp_proto/types/
backend_envelope.rs

1use prost::Message;
2use xmtp_common::sha256_array;
3
4use crate::xmtp::backend::v1::ClientEnvelope;
5
6/// Canonical outer encoding and its hash. Inner payload bytes are unchanged.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CanonicalEnvelope {
9    /// Prost's canonical field ordering for the outer envelope.
10    pub bytes: Vec<u8>,
11    /// SHA-256 of `bytes`, used as the publish idempotency key.
12    pub hash: [u8; 32],
13}
14
15/// Encode the outer envelope once for storage, retries, and hash matching.
16///
17/// Inner payload bytes are copied exactly as supplied. This function does not
18/// validate the payload or calculate the separate client MLS message ID.
19// implements: API-211
20pub 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    /// Same vector on native and wasm.
39    // verifies: API-211
40    #[xmtp_common::test(unwrap_try = true)]
41    fn all_payloads_have_stable_canonical_bytes_and_distinct_outer_hashes() {
42        // Reordered inner fields and an unknown outer field are noncanonical.
43        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}