solana_offchain_message/
lib.rs

1//! Off-chain message container for storing non-transaction messages.
2#![cfg_attr(docsrs, feature(doc_auto_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        let version = data[Self::SIGNING_DOMAIN.len()];
200        let data = &data[Self::SIGNING_DOMAIN.len().saturating_add(1)..];
201        match version {
202            0 => Ok(Self::V0(v0::OffchainMessage::deserialize(data)?)),
203            _ => Err(SanitizeError::ValueOutOfBounds),
204        }
205    }
206
207    /// Compute the hash of the off-chain message
208    pub fn hash(&self) -> Result<Hash, SanitizeError> {
209        match self {
210            Self::V0(_) => v0::OffchainMessage::hash(&self.serialize()?),
211        }
212    }
213
214    pub fn get_version(&self) -> u8 {
215        match self {
216            Self::V0(_) => 0,
217        }
218    }
219
220    pub fn get_format(&self) -> MessageFormat {
221        match self {
222            Self::V0(msg) => msg.get_format(),
223        }
224    }
225
226    pub fn get_message(&self) -> &Vec<u8> {
227        match self {
228            Self::V0(msg) => msg.get_message(),
229        }
230    }
231
232    /// Sign the message with provided keypair
233    pub fn sign(&self, signer: &dyn Signer) -> Result<Signature, SanitizeError> {
234        Ok(signer.sign_message(&self.serialize()?))
235    }
236
237    #[cfg(feature = "verify")]
238    /// Verify that the message signature is valid for the given public key
239    pub fn verify(
240        &self,
241        signer: &solana_pubkey::Pubkey,
242        signature: &Signature,
243    ) -> Result<bool, SanitizeError> {
244        Ok(signature.verify(signer.as_ref(), &self.serialize()?))
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use {super::*, solana_keypair::Keypair, std::str::FromStr};
251
252    #[test]
253    fn test_offchain_message_ascii() {
254        let message = OffchainMessage::new(0, b"Test Message").unwrap();
255        assert_eq!(message.get_version(), 0);
256        assert_eq!(message.get_format(), MessageFormat::RestrictedAscii);
257        assert_eq!(message.get_message().as_slice(), b"Test Message");
258        assert!(
259            matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::RestrictedAscii)
260        );
261        let serialized = [
262            255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 0, 12, 0,
263            84, 101, 115, 116, 32, 77, 101, 115, 115, 97, 103, 101,
264        ];
265        let hash = Hash::from_str("HG5JydBGjtjTfD3sSn21ys5NTWPpXzmqifiGC2BVUjkD").unwrap();
266        assert_eq!(message.serialize().unwrap(), serialized);
267        assert_eq!(message.hash().unwrap(), hash);
268        assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
269    }
270
271    #[test]
272    fn test_offchain_message_utf8() {
273        let message = OffchainMessage::new(0, "Тестовое сообщение".as_bytes()).unwrap();
274        assert_eq!(message.get_version(), 0);
275        assert_eq!(message.get_format(), MessageFormat::LimitedUtf8);
276        assert_eq!(
277            message.get_message().as_slice(),
278            "Тестовое сообщение".as_bytes()
279        );
280        assert!(
281            matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::LimitedUtf8)
282        );
283        let serialized = [
284            255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 1, 35, 0,
285            208, 162, 208, 181, 209, 129, 209, 130, 208, 190, 208, 178, 208, 190, 208, 181, 32,
286            209, 129, 208, 190, 208, 190, 208, 177, 209, 137, 208, 181, 208, 189, 208, 184, 208,
287            181,
288        ];
289        let hash = Hash::from_str("6GXTveatZQLexkX4WeTpJ3E7uk1UojRXpKp43c4ArSun").unwrap();
290        assert_eq!(message.serialize().unwrap(), serialized);
291        assert_eq!(message.hash().unwrap(), hash);
292        assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
293    }
294
295    #[test]
296    fn test_offchain_message_sign_and_verify() {
297        let message = OffchainMessage::new(0, b"Test Message").unwrap();
298        let keypair = Keypair::new();
299        let signature = message.sign(&keypair).unwrap();
300        assert!(message.verify(&keypair.pubkey(), &signature).unwrap());
301    }
302}