Skip to main content

solana_offchain_message/
lib.rs

1//! Off-chain message container for storing non-transaction messages.
2#![cfg_attr(docsrs, feature(doc_cfg))]
3use {
4    num_enum::{IntoPrimitive, TryFromPrimitive},
5    solana_hash::Hash,
6    solana_sanitize::SanitizeError,
7    solana_signature::Signature,
8    solana_signer::Signer,
9};
10
11#[cfg(test)]
12static_assertions::const_assert_eq!(OffchainMessage::HEADER_LEN, 17);
13#[cfg(test)]
14static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN, 65515);
15#[cfg(test)]
16static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN_LEDGER, 1212);
17
18/// Check if given bytes contain only printable ASCII characters
19pub fn is_printable_ascii(data: &[u8]) -> bool {
20    for &char in data {
21        if !(0x20..=0x7e).contains(&char) {
22            return false;
23        }
24    }
25    true
26}
27
28/// Check if given bytes contain valid UTF8 string
29pub fn is_utf8(data: &[u8]) -> bool {
30    std::str::from_utf8(data).is_ok()
31}
32
33#[repr(u8)]
34#[derive(Debug, PartialEq, Eq, Copy, Clone, TryFromPrimitive, IntoPrimitive)]
35pub enum MessageFormat {
36    RestrictedAscii,
37    LimitedUtf8,
38    ExtendedUtf8,
39}
40
41#[allow(clippy::arithmetic_side_effects)]
42pub mod v0 {
43    use {
44        super::{is_printable_ascii, is_utf8, MessageFormat, OffchainMessage as Base},
45        solana_hash::Hash,
46        solana_packet::PACKET_DATA_SIZE,
47        solana_sanitize::SanitizeError,
48        solana_sha256_hasher::Hasher,
49    };
50
51    /// OffchainMessage Version 0.
52    /// Struct always contains a non-empty valid message.
53    #[derive(Debug, PartialEq, Eq, Clone)]
54    pub struct OffchainMessage {
55        format: MessageFormat,
56        message: Vec<u8>,
57    }
58
59    impl OffchainMessage {
60        // Header Length = Message Format (1) + Message Length (2)
61        pub const HEADER_LEN: usize = 3;
62        // Max length of the OffchainMessage
63        pub const MAX_LEN: usize = u16::MAX as usize - Base::HEADER_LEN - Self::HEADER_LEN;
64        // Max Length of the OffchainMessage supported by the Ledger
65        pub const MAX_LEN_LEDGER: usize = PACKET_DATA_SIZE - Base::HEADER_LEN - Self::HEADER_LEN;
66
67        /// Construct a new OffchainMessage object from the given message
68        pub fn new(message: &[u8]) -> Result<Self, SanitizeError> {
69            let format = if message.is_empty() {
70                return Err(SanitizeError::InvalidValue);
71            } else if message.len() <= OffchainMessage::MAX_LEN_LEDGER {
72                if is_printable_ascii(message) {
73                    MessageFormat::RestrictedAscii
74                } else if is_utf8(message) {
75                    MessageFormat::LimitedUtf8
76                } else {
77                    return Err(SanitizeError::InvalidValue);
78                }
79            } else if message.len() <= OffchainMessage::MAX_LEN {
80                if is_utf8(message) {
81                    MessageFormat::ExtendedUtf8
82                } else {
83                    return Err(SanitizeError::InvalidValue);
84                }
85            } else {
86                return Err(SanitizeError::ValueOutOfBounds);
87            };
88            Ok(Self {
89                format,
90                message: message.to_vec(),
91            })
92        }
93
94        /// Serialize the message to bytes, including the full header
95        pub fn serialize(&self, data: &mut Vec<u8>) -> Result<(), SanitizeError> {
96            // invalid messages shouldn't be possible, but a quick sanity check never hurts
97            assert!(!self.message.is_empty() && self.message.len() <= Self::MAX_LEN);
98            data.reserve(Self::HEADER_LEN.saturating_add(self.message.len()));
99            // format
100            data.push(self.format.into());
101            // message length
102            data.extend_from_slice(&(self.message.len() as u16).to_le_bytes());
103            // message
104            data.extend_from_slice(&self.message);
105            Ok(())
106        }
107
108        /// Deserialize the message from bytes that include a full header
109        pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
110            // validate data length
111            if data.len() <= Self::HEADER_LEN || data.len() > Self::HEADER_LEN + Self::MAX_LEN {
112                return Err(SanitizeError::ValueOutOfBounds);
113            }
114            // decode header
115            let format =
116                MessageFormat::try_from(data[0]).map_err(|_| SanitizeError::InvalidValue)?;
117            let message_len = u16::from_le_bytes([data[1], data[2]]) as usize;
118            // check header
119            if Self::HEADER_LEN.saturating_add(message_len) != data.len() {
120                return Err(SanitizeError::InvalidValue);
121            }
122            let message = &data[Self::HEADER_LEN..];
123            // check format
124            let is_valid = match format {
125                MessageFormat::RestrictedAscii => {
126                    (message.len() <= Self::MAX_LEN_LEDGER) && is_printable_ascii(message)
127                }
128                MessageFormat::LimitedUtf8 => {
129                    (message.len() <= Self::MAX_LEN_LEDGER) && is_utf8(message)
130                }
131                MessageFormat::ExtendedUtf8 => (message.len() <= Self::MAX_LEN) && is_utf8(message),
132            };
133
134            if is_valid {
135                Ok(Self {
136                    format,
137                    message: message.to_vec(),
138                })
139            } else {
140                Err(SanitizeError::InvalidValue)
141            }
142        }
143
144        /// Compute the SHA256 hash of the serialized off-chain message
145        pub fn hash(serialized_message: &[u8]) -> Result<Hash, SanitizeError> {
146            let mut hasher = Hasher::default();
147            hasher.hash(serialized_message);
148            Ok(hasher.result())
149        }
150
151        pub fn get_format(&self) -> MessageFormat {
152            self.format
153        }
154
155        pub fn get_message(&self) -> &Vec<u8> {
156            &self.message
157        }
158    }
159}
160
161#[derive(Debug, PartialEq, Eq, Clone)]
162pub enum OffchainMessage {
163    V0(v0::OffchainMessage),
164}
165
166impl OffchainMessage {
167    pub const SIGNING_DOMAIN: &'static [u8] = b"\xffsolana offchain";
168    // Header Length = Signing Domain (16) + Header Version (1)
169    pub const HEADER_LEN: usize = Self::SIGNING_DOMAIN.len() + 1;
170
171    /// Construct a new OffchainMessage object from the given version and message
172    pub fn new(version: u8, message: &[u8]) -> Result<Self, SanitizeError> {
173        match version {
174            0 => Ok(Self::V0(v0::OffchainMessage::new(message)?)),
175            _ => Err(SanitizeError::ValueOutOfBounds),
176        }
177    }
178
179    /// Serialize the off-chain message to bytes including full header
180    pub fn serialize(&self) -> Result<Vec<u8>, SanitizeError> {
181        // serialize signing domain
182        let mut data = Self::SIGNING_DOMAIN.to_vec();
183
184        // serialize version and call version specific serializer
185        match self {
186            Self::V0(msg) => {
187                data.push(0);
188                msg.serialize(&mut data)?;
189            }
190        }
191        Ok(data)
192    }
193
194    /// Deserialize the off-chain message from bytes that include full header
195    pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
196        if data.len() <= Self::HEADER_LEN {
197            return Err(SanitizeError::ValueOutOfBounds);
198        }
199        if !data.starts_with(Self::SIGNING_DOMAIN) {
200            return Err(SanitizeError::InvalidValue);
201        }
202        let version = data[Self::SIGNING_DOMAIN.len()];
203        let data = &data[Self::SIGNING_DOMAIN.len().saturating_add(1)..];
204        match version {
205            0 => Ok(Self::V0(v0::OffchainMessage::deserialize(data)?)),
206            _ => Err(SanitizeError::ValueOutOfBounds),
207        }
208    }
209
210    /// Compute the hash of the off-chain message
211    pub fn hash(&self) -> Result<Hash, SanitizeError> {
212        match self {
213            Self::V0(_) => v0::OffchainMessage::hash(&self.serialize()?),
214        }
215    }
216
217    pub fn get_version(&self) -> u8 {
218        match self {
219            Self::V0(_) => 0,
220        }
221    }
222
223    pub fn get_format(&self) -> MessageFormat {
224        match self {
225            Self::V0(msg) => msg.get_format(),
226        }
227    }
228
229    pub fn get_message(&self) -> &Vec<u8> {
230        match self {
231            Self::V0(msg) => msg.get_message(),
232        }
233    }
234
235    /// Sign the message with provided keypair
236    pub fn sign(&self, signer: &dyn Signer) -> Result<Signature, SanitizeError> {
237        Ok(signer.sign_message(&self.serialize()?))
238    }
239
240    #[cfg(feature = "verify")]
241    /// Verify that the message signature is valid for the given public key
242    pub fn verify(
243        &self,
244        signer: &solana_pubkey::Pubkey,
245        signature: &Signature,
246    ) -> Result<bool, SanitizeError> {
247        Ok(signature.verify(signer.as_ref(), &self.serialize()?))
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use {super::*, solana_keypair::Keypair, std::str::FromStr};
254
255    #[test]
256    fn test_offchain_message_rejects_invalid_signing_domain() {
257        let message = OffchainMessage::new(0, b"Test Message").unwrap();
258        let serialized = message.serialize().unwrap();
259
260        for index in 0..OffchainMessage::SIGNING_DOMAIN.len() {
261            let mut altered = serialized.clone();
262            altered[index] ^= 1;
263
264            assert_eq!(
265                OffchainMessage::deserialize(&altered),
266                Err(SanitizeError::InvalidValue),
267            );
268        }
269    }
270
271    #[test]
272    fn test_offchain_message_ascii() {
273        let message = OffchainMessage::new(0, b"Test Message").unwrap();
274        assert_eq!(message.get_version(), 0);
275        assert_eq!(message.get_format(), MessageFormat::RestrictedAscii);
276        assert_eq!(message.get_message().as_slice(), b"Test Message");
277        assert!(
278            matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::RestrictedAscii)
279        );
280        let serialized = [
281            255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 0, 12, 0,
282            84, 101, 115, 116, 32, 77, 101, 115, 115, 97, 103, 101,
283        ];
284        let hash = Hash::from_str("HG5JydBGjtjTfD3sSn21ys5NTWPpXzmqifiGC2BVUjkD").unwrap();
285        assert_eq!(message.serialize().unwrap(), serialized);
286        assert_eq!(message.hash().unwrap(), hash);
287        assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
288    }
289
290    #[test]
291    fn test_offchain_message_utf8() {
292        let message = OffchainMessage::new(0, "Тестовое сообщение".as_bytes()).unwrap();
293        assert_eq!(message.get_version(), 0);
294        assert_eq!(message.get_format(), MessageFormat::LimitedUtf8);
295        assert_eq!(
296            message.get_message().as_slice(),
297            "Тестовое сообщение".as_bytes()
298        );
299        assert!(
300            matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::LimitedUtf8)
301        );
302        let serialized = [
303            255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 1, 35, 0,
304            208, 162, 208, 181, 209, 129, 209, 130, 208, 190, 208, 178, 208, 190, 208, 181, 32,
305            209, 129, 208, 190, 208, 190, 208, 177, 209, 137, 208, 181, 208, 189, 208, 184, 208,
306            181,
307        ];
308        let hash = Hash::from_str("6GXTveatZQLexkX4WeTpJ3E7uk1UojRXpKp43c4ArSun").unwrap();
309        assert_eq!(message.serialize().unwrap(), serialized);
310        assert_eq!(message.hash().unwrap(), hash);
311        assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
312    }
313
314    #[test]
315    fn test_offchain_message_sign_and_verify() {
316        let message = OffchainMessage::new(0, b"Test Message").unwrap();
317        let keypair = Keypair::new();
318        let signature = message.sign(&keypair).unwrap();
319        assert!(message.verify(&keypair.pubkey(), &signature).unwrap());
320    }
321}