solana_sdk/
offchain_message.rs

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