1#![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
18pub 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
28pub 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 #[derive(Debug, PartialEq, Eq, Clone)]
54 pub struct OffchainMessage {
55 format: MessageFormat,
56 message: Vec<u8>,
57 }
58
59 impl OffchainMessage {
60 pub const HEADER_LEN: usize = 3;
62 pub const MAX_LEN: usize = u16::MAX as usize - Base::HEADER_LEN - Self::HEADER_LEN;
64 pub const MAX_LEN_LEDGER: usize = PACKET_DATA_SIZE - Base::HEADER_LEN - Self::HEADER_LEN;
66
67 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 pub fn serialize(&self, data: &mut Vec<u8>) -> Result<(), SanitizeError> {
96 assert!(!self.message.is_empty() && self.message.len() <= Self::MAX_LEN);
98 data.reserve(Self::HEADER_LEN.saturating_add(self.message.len()));
99 data.push(self.format.into());
101 data.extend_from_slice(&(self.message.len() as u16).to_le_bytes());
103 data.extend_from_slice(&self.message);
105 Ok(())
106 }
107
108 pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
110 if data.len() <= Self::HEADER_LEN || data.len() > Self::HEADER_LEN + Self::MAX_LEN {
112 return Err(SanitizeError::ValueOutOfBounds);
113 }
114 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 if Self::HEADER_LEN.saturating_add(message_len) != data.len() {
120 return Err(SanitizeError::InvalidValue);
121 }
122 let message = &data[Self::HEADER_LEN..];
123 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 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 pub const HEADER_LEN: usize = Self::SIGNING_DOMAIN.len() + 1;
170
171 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 pub fn serialize(&self) -> Result<Vec<u8>, SanitizeError> {
181 let mut data = Self::SIGNING_DOMAIN.to_vec();
183
184 match self {
186 Self::V0(msg) => {
187 data.push(0);
188 msg.serialize(&mut data)?;
189 }
190 }
191 Ok(data)
192 }
193
194 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 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 pub fn sign(&self, signer: &dyn Signer) -> Result<Signature, SanitizeError> {
234 Ok(signer.sign_message(&self.serialize()?))
235 }
236
237 #[cfg(feature = "verify")]
238 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}