Skip to main content

solana_message/versions/
mod.rs

1#[cfg(any(feature = "wincode", feature = "serde"))]
2use alloc::vec::Vec;
3#[cfg(feature = "frozen-abi")]
4use solana_frozen_abi_macro::{frozen_abi, AbiEnumVisitor, AbiExample};
5#[cfg(feature = "std")]
6use std::collections::HashSet;
7use {
8    crate::{
9        compiled_instruction::CompiledInstruction, legacy::Message as LegacyMessage,
10        v0::MessageAddressTableLookup, MessageHeader,
11    },
12    solana_address::Address,
13    solana_hash::Hash,
14    solana_sanitize::{Sanitize, SanitizeError},
15};
16#[cfg(feature = "serde")]
17use {
18    core::fmt,
19    serde::{
20        de::{self, Deserializer, SeqAccess, Unexpected, Visitor},
21        ser::{SerializeTuple, Serializer},
22    },
23    serde_derive::{Deserialize, Serialize},
24};
25#[cfg(feature = "wincode")]
26use {
27    core::mem::MaybeUninit,
28    wincode::{
29        config::Config,
30        io::{Reader, Writer},
31        ReadResult, SchemaRead, SchemaReadContext, SchemaWrite, WriteResult,
32    },
33};
34
35mod sanitized;
36pub mod v0;
37pub mod v1;
38
39pub use sanitized::*;
40
41/// Bit mask that indicates whether a serialized message is versioned.
42pub const MESSAGE_VERSION_PREFIX: u8 = 0x80;
43
44/// Either a legacy message, v0 or a v1 message.
45///
46/// # Serialization
47///
48/// If the first bit is set, the remaining 7 bits will be used to determine
49/// which message version is serialized starting from version `0`. If the first
50/// is bit is not set, all bytes are used to encode the legacy `Message`
51/// format.
52#[cfg_attr(
53    feature = "frozen-abi",
54    frozen_abi(digest = "9xQQLkQntX2QKgwxbbpeuNrs5V2WopsBa11su46WWCro"),
55    derive(AbiEnumVisitor, AbiExample)
56)]
57#[derive(Debug, PartialEq, Eq, Clone)]
58pub enum VersionedMessage {
59    Legacy(LegacyMessage),
60    V0(v0::Message),
61    V1(v1::Message),
62}
63
64impl VersionedMessage {
65    pub fn sanitize(&self) -> Result<(), SanitizeError> {
66        match self {
67            Self::Legacy(message) => message.sanitize(),
68            Self::V0(message) => message.sanitize(),
69            Self::V1(message) => message.sanitize(),
70        }
71    }
72
73    pub fn header(&self) -> &MessageHeader {
74        match self {
75            Self::Legacy(message) => &message.header,
76            Self::V0(message) => &message.header,
77            Self::V1(message) => &message.header,
78        }
79    }
80
81    pub fn static_account_keys(&self) -> &[Address] {
82        match self {
83            Self::Legacy(message) => &message.account_keys,
84            Self::V0(message) => &message.account_keys,
85            Self::V1(message) => &message.account_keys,
86        }
87    }
88
89    pub fn address_table_lookups(&self) -> Option<&[MessageAddressTableLookup]> {
90        match self {
91            Self::Legacy(_) => None,
92            Self::V0(message) => Some(&message.address_table_lookups),
93            Self::V1(_) => None,
94        }
95    }
96
97    /// Returns true if the account at the specified index signed this
98    /// message.
99    pub fn is_signer(&self, index: usize) -> bool {
100        index < usize::from(self.header().num_required_signatures)
101    }
102
103    /// Returns true if the account at the specified index is writable by the
104    /// instructions in this message. Since dynamically loaded addresses can't
105    /// have write locks demoted without loading addresses, this shouldn't be
106    /// used in the runtime.
107    #[cfg(feature = "std")]
108    pub fn is_maybe_writable(
109        &self,
110        index: usize,
111        reserved_account_keys: Option<&HashSet<Address>>,
112    ) -> bool {
113        match self {
114            Self::Legacy(message) => message.is_maybe_writable(index, reserved_account_keys),
115            Self::V0(message) => message.is_maybe_writable(index, reserved_account_keys),
116            Self::V1(message) => message.is_maybe_writable(index, reserved_account_keys),
117        }
118    }
119
120    /// Returns true if the account at the specified index is an input to some
121    /// program instruction in this message.
122    fn is_instruction_account(&self, key_index: usize) -> bool {
123        if let Ok(key_index) = u8::try_from(key_index) {
124            self.instructions()
125                .iter()
126                .any(|ix| ix.accounts.contains(&key_index))
127        } else {
128            false
129        }
130    }
131
132    pub fn is_invoked(&self, key_index: usize) -> bool {
133        match self {
134            Self::Legacy(message) => message.is_key_called_as_program(key_index),
135            Self::V0(message) => message.is_key_called_as_program(key_index),
136            Self::V1(message) => message.is_key_called_as_program(key_index),
137        }
138    }
139
140    /// Returns true if the account at the specified index is not invoked as a
141    /// program or, if invoked, is passed to a program.
142    pub fn is_non_loader_key(&self, key_index: usize) -> bool {
143        !self.is_invoked(key_index) || self.is_instruction_account(key_index)
144    }
145
146    pub fn recent_blockhash(&self) -> &Hash {
147        match self {
148            Self::Legacy(message) => &message.recent_blockhash,
149            Self::V0(message) => &message.recent_blockhash,
150            Self::V1(message) => &message.lifetime_specifier,
151        }
152    }
153
154    pub fn set_recent_blockhash(&mut self, recent_blockhash: Hash) {
155        match self {
156            Self::Legacy(message) => message.recent_blockhash = recent_blockhash,
157            Self::V0(message) => message.recent_blockhash = recent_blockhash,
158            Self::V1(message) => message.lifetime_specifier = recent_blockhash,
159        }
160    }
161
162    /// Program instructions that will be executed in sequence and committed in
163    /// one atomic transaction if all succeed.
164    #[inline(always)]
165    pub fn instructions(&self) -> &[CompiledInstruction] {
166        match self {
167            Self::Legacy(message) => &message.instructions,
168            Self::V0(message) => &message.instructions,
169            Self::V1(message) => &message.instructions,
170        }
171    }
172
173    #[cfg(feature = "wincode")]
174    pub fn serialize(&self) -> Vec<u8> {
175        wincode::serialize(self).unwrap()
176    }
177
178    #[cfg(all(feature = "wincode", feature = "blake3"))]
179    /// Compute the blake3 hash of this transaction's message
180    pub fn hash(&self) -> Hash {
181        let message_bytes = self.serialize();
182        Self::hash_raw_message(&message_bytes)
183    }
184
185    #[cfg(feature = "blake3")]
186    /// Compute the blake3 hash of a raw transaction message
187    pub fn hash_raw_message(message_bytes: &[u8]) -> Hash {
188        use blake3::traits::digest::Digest;
189        let mut hasher = blake3::Hasher::new();
190        hasher.update(b"solana-tx-message-v1");
191        hasher.update(message_bytes);
192        let hash_bytes: [u8; solana_hash::HASH_BYTES] = hasher.finalize().into();
193        hash_bytes.into()
194    }
195}
196
197impl Default for VersionedMessage {
198    fn default() -> Self {
199        Self::Legacy(LegacyMessage::default())
200    }
201}
202
203#[cfg(feature = "serde")]
204impl serde::Serialize for VersionedMessage {
205    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
206    where
207        S: Serializer,
208    {
209        match self {
210            Self::Legacy(message) => {
211                let mut seq = serializer.serialize_tuple(1)?;
212                seq.serialize_element(message)?;
213                seq.end()
214            }
215            Self::V0(message) => {
216                let mut seq = serializer.serialize_tuple(2)?;
217                seq.serialize_element(&MESSAGE_VERSION_PREFIX)?;
218                seq.serialize_element(message)?;
219                seq.end()
220            }
221            Self::V1(message) => {
222                // Note that this format does not match the wire format per SIMD-0385.
223
224                let mut seq = serializer.serialize_tuple(2)?;
225                seq.serialize_element(&crate::v1::V1_PREFIX)?;
226                seq.serialize_element(message)?;
227                seq.end()
228            }
229        }
230    }
231}
232
233#[cfg(feature = "serde")]
234enum MessagePrefix {
235    Legacy(u8),
236    Versioned(u8),
237}
238
239#[cfg(feature = "serde")]
240impl<'de> serde::Deserialize<'de> for MessagePrefix {
241    fn deserialize<D>(deserializer: D) -> Result<MessagePrefix, D::Error>
242    where
243        D: Deserializer<'de>,
244    {
245        struct PrefixVisitor;
246
247        impl Visitor<'_> for PrefixVisitor {
248            type Value = MessagePrefix;
249
250            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
251                formatter.write_str("message prefix byte")
252            }
253
254            // Serde's integer visitors bubble up to u64 so check the prefix
255            // with this function instead of visit_u8. This approach is
256            // necessary because serde_json directly calls visit_u64 for
257            // unsigned integers.
258            fn visit_u64<E: de::Error>(self, value: u64) -> Result<MessagePrefix, E> {
259                if value > u8::MAX as u64 {
260                    Err(de::Error::invalid_type(Unexpected::Unsigned(value), &self))?;
261                }
262
263                let byte = value as u8;
264                if byte & MESSAGE_VERSION_PREFIX != 0 {
265                    Ok(MessagePrefix::Versioned(byte & !MESSAGE_VERSION_PREFIX))
266                } else {
267                    Ok(MessagePrefix::Legacy(byte))
268                }
269            }
270        }
271
272        deserializer.deserialize_u8(PrefixVisitor)
273    }
274}
275
276#[cfg(feature = "serde")]
277impl<'de> serde::Deserialize<'de> for VersionedMessage {
278    fn deserialize<D>(deserializer: D) -> Result<VersionedMessage, D::Error>
279    where
280        D: Deserializer<'de>,
281    {
282        struct MessageVisitor;
283
284        impl<'de> Visitor<'de> for MessageVisitor {
285            type Value = VersionedMessage;
286
287            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
288                formatter.write_str("message bytes")
289            }
290
291            fn visit_seq<A>(self, mut seq: A) -> Result<VersionedMessage, A::Error>
292            where
293                A: SeqAccess<'de>,
294            {
295                let prefix: MessagePrefix = seq
296                    .next_element()?
297                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
298
299                match prefix {
300                    MessagePrefix::Legacy(num_required_signatures) => {
301                        // The remaining fields of the legacy Message struct after the first byte.
302                        #[derive(Serialize, Deserialize)]
303                        struct RemainingLegacyMessage {
304                            pub num_readonly_signed_accounts: u8,
305                            pub num_readonly_unsigned_accounts: u8,
306                            #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
307                            pub account_keys: Vec<Address>,
308                            pub recent_blockhash: Hash,
309                            #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
310                            pub instructions: Vec<CompiledInstruction>,
311                        }
312
313                        let message: RemainingLegacyMessage =
314                            seq.next_element()?.ok_or_else(|| {
315                                // will never happen since tuple length is always 2
316                                de::Error::invalid_length(1, &self)
317                            })?;
318
319                        Ok(VersionedMessage::Legacy(LegacyMessage {
320                            header: MessageHeader {
321                                num_required_signatures,
322                                num_readonly_signed_accounts: message.num_readonly_signed_accounts,
323                                num_readonly_unsigned_accounts: message
324                                    .num_readonly_unsigned_accounts,
325                            },
326                            account_keys: message.account_keys,
327                            recent_blockhash: message.recent_blockhash,
328                            instructions: message.instructions,
329                        }))
330                    }
331                    MessagePrefix::Versioned(version) => {
332                        match version {
333                            0 => {
334                                Ok(VersionedMessage::V0(seq.next_element()?.ok_or_else(
335                                    || {
336                                        // will never happen since tuple length is always 2
337                                        de::Error::invalid_length(1, &self)
338                                    },
339                                )?))
340                            }
341                            1 => {
342                                Ok(VersionedMessage::V1(seq.next_element()?.ok_or_else(
343                                    || {
344                                        // will never happen since tuple length is always 2
345                                        de::Error::invalid_length(1, &self)
346                                    },
347                                )?))
348                            }
349                            127 => {
350                                // 0xff is used as the first byte of the off-chain messages
351                                // which corresponds to version 127 of the versioned messages.
352                                // This explicit check is added to prevent the usage of version 127
353                                // in the runtime as a valid transaction.
354                                Err(de::Error::custom("off-chain messages are not accepted"))
355                            }
356                            _ => Err(de::Error::invalid_value(
357                                de::Unexpected::Unsigned(version as u64),
358                                &"a valid transaction message version",
359                            )),
360                        }
361                    }
362                }
363            }
364        }
365
366        deserializer.deserialize_tuple(2, MessageVisitor)
367    }
368}
369
370#[cfg(feature = "wincode")]
371unsafe impl<C: Config> SchemaWrite<C> for VersionedMessage {
372    type Src = Self;
373
374    // V0 and V1 add +1 for message version prefix
375    #[allow(clippy::arithmetic_side_effects)]
376    #[inline(always)]
377    fn size_of(src: &Self::Src) -> WriteResult<usize> {
378        match src {
379            VersionedMessage::Legacy(message) => {
380                <LegacyMessage as SchemaWrite<C>>::size_of(message)
381            }
382            VersionedMessage::V0(message) => {
383                Ok(1 + <v0::Message as SchemaWrite<C>>::size_of(message)?)
384            }
385            VersionedMessage::V1(message) => Ok(1 + message.size()),
386        }
387    }
388
389    // V0 and V1 add +1 for message version prefix
390    #[allow(clippy::arithmetic_side_effects)]
391    #[inline(always)]
392    fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
393        match src {
394            VersionedMessage::Legacy(message) => {
395                <LegacyMessage as SchemaWrite<C>>::write(writer, message)
396            }
397            VersionedMessage::V0(message) => {
398                <u8 as SchemaWrite<C>>::write(&mut writer, &MESSAGE_VERSION_PREFIX)?;
399                <v0::Message as SchemaWrite<C>>::write(writer, message)
400            }
401            VersionedMessage::V1(message) => {
402                <u8 as SchemaWrite<C>>::write(writer.by_ref(), &crate::v1::V1_PREFIX)?;
403                <v1::Message as SchemaWrite<C>>::write(writer, message)
404            }
405        }
406    }
407}
408
409#[cfg(feature = "wincode")]
410unsafe impl<'de, C: Config> SchemaReadContext<'de, C, u8> for VersionedMessage {
411    type Dst = Self;
412
413    fn read_with_context(
414        discriminant: u8,
415        reader: impl Reader<'de>,
416        dst: &mut MaybeUninit<Self::Dst>,
417    ) -> ReadResult<()> {
418        // If the first bit is set, the remaining 7 bits will be used to determine
419        // which message version is serialized starting from version `0`. If the first
420        // is bit is not set, all bytes are used to encode the legacy `Message`
421        // format.
422        if discriminant & MESSAGE_VERSION_PREFIX != 0 {
423            use wincode::error::invalid_tag_encoding;
424
425            let version = discriminant & !MESSAGE_VERSION_PREFIX;
426            return match version {
427                0 => {
428                    let msg = <v0::Message as SchemaRead<C>>::get(reader)?;
429                    dst.write(VersionedMessage::V0(msg));
430                    Ok(())
431                }
432                1 => {
433                    let message = <v1::Message as SchemaRead<C>>::get(reader)?;
434                    dst.write(VersionedMessage::V1(message));
435
436                    Ok(())
437                }
438                _ => Err(invalid_tag_encoding(version as usize)),
439            };
440        };
441        let legacy =
442            <LegacyMessage as SchemaReadContext<C, _>>::get_with_context(discriminant, reader)?;
443        dst.write(VersionedMessage::Legacy(legacy));
444
445        Ok(())
446    }
447}
448#[cfg(feature = "wincode")]
449unsafe impl<'de, C: Config> SchemaRead<'de, C> for VersionedMessage {
450    type Dst = Self;
451
452    #[inline]
453    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
454        let discriminant = reader.take_byte()?;
455        <VersionedMessage as SchemaReadContext<C, _>>::read_with_context(discriminant, reader, dst)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use {
462        super::*,
463        crate::{
464            v0::MessageAddressTableLookup,
465            v1::{MAX_HEAP_SIZE, MIN_HEAP_SIZE, V1_PREFIX},
466        },
467        alloc::vec,
468        proptest::{
469            collection::vec,
470            option::of,
471            prelude::{any, Just},
472            prop_compose, proptest,
473        },
474        solana_instruction::{AccountMeta, Instruction},
475    };
476
477    #[derive(Clone, Debug)]
478    struct TestMessageData {
479        required_signatures: u8,
480        lifetime: [u8; 32],
481        accounts: Vec<[u8; 32]>,
482        priority_fee: Option<u64>,
483        compute_unit_limit: Option<u32>,
484        loaded_accounts_data_size_limit: Option<u32>,
485        heap_size: Option<u32>,
486        program_id_index: u8,
487        instr_accounts: Vec<u8>,
488        data: Vec<u8>,
489    }
490
491    #[test]
492    fn test_legacy_message_serialization() {
493        let program_id0 = Address::new_unique();
494        let program_id1 = Address::new_unique();
495        let id0 = Address::new_unique();
496        let id1 = Address::new_unique();
497        let id2 = Address::new_unique();
498        let id3 = Address::new_unique();
499        let instructions = vec![
500            Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
501            Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, true)]),
502            Instruction::new_with_bincode(
503                program_id1,
504                &0,
505                vec![AccountMeta::new_readonly(id2, false)],
506            ),
507            Instruction::new_with_bincode(
508                program_id1,
509                &0,
510                vec![AccountMeta::new_readonly(id3, true)],
511            ),
512        ];
513
514        let mut message = LegacyMessage::new(&instructions, Some(&id1));
515        message.recent_blockhash = Hash::new_unique();
516        let wrapped_message = VersionedMessage::Legacy(message.clone());
517
518        // bincode
519        {
520            let bytes = bincode::serialize(&message).unwrap();
521            assert_eq!(bytes, bincode::serialize(&wrapped_message).unwrap());
522
523            let message_from_bytes: LegacyMessage = bincode::deserialize(&bytes).unwrap();
524            let wrapped_message_from_bytes: VersionedMessage =
525                bincode::deserialize(&bytes).unwrap();
526
527            assert_eq!(message, message_from_bytes);
528            assert_eq!(wrapped_message, wrapped_message_from_bytes);
529        }
530
531        // serde_json
532        {
533            let string = serde_json::to_string(&message).unwrap();
534            let message_from_string: LegacyMessage = serde_json::from_str(&string).unwrap();
535            assert_eq!(message, message_from_string);
536        }
537    }
538
539    #[test]
540    fn test_versioned_message_serialization() {
541        let message = VersionedMessage::V0(v0::Message {
542            header: MessageHeader {
543                num_required_signatures: 1,
544                num_readonly_signed_accounts: 0,
545                num_readonly_unsigned_accounts: 0,
546            },
547            recent_blockhash: Hash::new_unique(),
548            account_keys: vec![Address::new_unique()],
549            address_table_lookups: vec![
550                MessageAddressTableLookup {
551                    account_key: Address::new_unique(),
552                    writable_indexes: vec![1],
553                    readonly_indexes: vec![0],
554                },
555                MessageAddressTableLookup {
556                    account_key: Address::new_unique(),
557                    writable_indexes: vec![0],
558                    readonly_indexes: vec![1],
559                },
560            ],
561            instructions: vec![CompiledInstruction {
562                program_id_index: 1,
563                accounts: vec![0, 2, 3, 4],
564                data: vec![],
565            }],
566        });
567
568        let bytes = bincode::serialize(&message).unwrap();
569        let message_from_bytes: VersionedMessage = bincode::deserialize(&bytes).unwrap();
570        assert_eq!(message, message_from_bytes);
571
572        let string = serde_json::to_string(&message).unwrap();
573        let message_from_string: VersionedMessage = serde_json::from_str(&string).unwrap();
574        assert_eq!(message, message_from_string);
575    }
576
577    prop_compose! {
578        fn generate_message_data()
579            (
580                // Generate between 12 and 64 accounts since we need at least the
581                // amount of `required_signatures`.
582                accounts in vec(any::<[u8; 32]>(), 12..=64),
583                lifetime in any::<[u8; 32]>(),
584                priority_fee in of(any::<u64>()),
585                compute_unit_limit in of(0..=1_400_000u32),
586                loaded_accounts_data_size_limit in of(0..=20_480u32),
587                // heap size must be a multiple of 1024 and between MIN_HEAP_SIZE
588                // and MAX_HEAP_SIZE if specified.
589                heap_size in of(MIN_HEAP_SIZE.saturating_div(1024)..=MAX_HEAP_SIZE.saturating_div(1024)),
590                required_signatures in 1..=12u8,
591            )
592            (
593                // The `program_id_index` cannot be 0 (payer).
594                program_id_index in 1u8..accounts.len() as u8,
595                // we need to have at least `required_signatures` accounts.
596                instr_accounts in vec(
597                    0u8..accounts.len() as u8,
598                    (required_signatures as usize)..=accounts.len(),
599                ),
600                // Keep instruction data relatively small to avoid hitting the maximum
601                // transaction size when combined with the accounts.
602                data in vec(any::<u8>(), 0..=2048),
603                accounts in Just(accounts),
604                lifetime in Just(lifetime),
605                priority_fee in Just(priority_fee),
606                compute_unit_limit in Just(compute_unit_limit),
607                loaded_accounts_data_size_limit in Just(loaded_accounts_data_size_limit),
608                heap_size in Just(heap_size.map(|size| size.saturating_mul(1024))),
609                required_signatures in Just(required_signatures),
610            ) -> TestMessageData
611        {
612            TestMessageData {
613                required_signatures,
614                lifetime,
615                accounts,
616                priority_fee,
617                compute_unit_limit,
618                loaded_accounts_data_size_limit,
619                heap_size,
620                program_id_index,
621                instr_accounts,
622                data,
623            }
624        }
625    }
626
627    proptest! {
628        #[test]
629        fn test_v1_message_raw_bytes_roundtrip(test_data in generate_message_data()) {
630            let accounts: Vec<Address> = test_data.accounts.into_iter()
631                .map(Address::new_from_array).collect();
632            let lifetime = Hash::new_from_array(test_data.lifetime);
633
634            let mut builder = v1::MessageBuilder::new()
635                .required_signatures(test_data.required_signatures)
636                .lifetime_specifier(lifetime)
637                .accounts(accounts)
638                .instruction(CompiledInstruction {
639                    program_id_index: test_data.program_id_index,
640                    accounts: test_data.instr_accounts,
641                    data: test_data.data,
642                });
643
644            // config values.
645            if let Some(priority_fee) = test_data.priority_fee {
646                builder = builder.priority_fee(priority_fee);
647            }
648            if let Some(compute_unit_limit) = test_data.compute_unit_limit {
649                builder = builder.compute_unit_limit(compute_unit_limit);
650            }
651            if let Some(loaded_accounts_data_size_limit) = test_data.loaded_accounts_data_size_limit {
652                builder = builder.loaded_accounts_data_size_limit(loaded_accounts_data_size_limit);
653            }
654            if let Some(heap_size) = test_data.heap_size {
655                builder = builder.heap_size(heap_size);
656            }
657
658            let message = builder.build().unwrap();
659
660            // Serialize V1 to raw bytes (without the version prefix).
661            let bytes = wincode::serialize(&message).unwrap();
662            // Deserialize from raw bytes.
663            let parsed = v1::deserialize(&bytes).unwrap();
664
665            // Messages should match.
666            assert_eq!(message, parsed);
667            assert_eq!(message, wincode::deserialize(&bytes).unwrap());
668
669            // Wrap in VersionedMessage and test `serialize()`.
670            let versioned = VersionedMessage::V1(message);
671            let serialized = versioned.serialize();
672
673            // Assert that everything worked:
674            // - serialized message is not empty.
675            // - first byte is the version prefix with the correct version.
676            // - remaining bytes match the original serialized message.
677            assert!(!serialized.is_empty());
678            assert_eq!(serialized[0], V1_PREFIX);
679            assert_eq!(&serialized[1..], bytes.as_slice());
680        }
681    }
682
683    #[test]
684    fn test_v1_versioned_message_json_roundtrip() {
685        let msg = v1::MessageBuilder::new()
686            .required_signatures(1)
687            .lifetime_specifier(Hash::new_unique())
688            .accounts(vec![Address::new_unique(), Address::new_unique()])
689            .priority_fee(1000)
690            .compute_unit_limit(200_000)
691            .instruction(CompiledInstruction {
692                program_id_index: 1,
693                accounts: vec![0],
694                data: vec![1, 2, 3, 4],
695            })
696            .build()
697            .unwrap();
698
699        let vm = VersionedMessage::V1(msg);
700        let s = serde_json::to_string(&vm).unwrap();
701        let back: VersionedMessage = serde_json::from_str(&s).unwrap();
702        assert_eq!(vm, back);
703    }
704
705    #[cfg(feature = "wincode")]
706    #[test]
707    fn test_v1_wincode_roundtrip() {
708        let test_messages = [
709            // Minimal message
710            v1::MessageBuilder::new()
711                .required_signatures(1)
712                .lifetime_specifier(Hash::new_unique())
713                .accounts(vec![Address::new_unique(), Address::new_unique()])
714                .instruction(CompiledInstruction {
715                    program_id_index: 1,
716                    accounts: vec![0],
717                    data: vec![],
718                })
719                .build()
720                .unwrap(),
721            // With config
722            v1::MessageBuilder::new()
723                .required_signatures(1)
724                .lifetime_specifier(Hash::new_unique())
725                .accounts(vec![Address::new_unique(), Address::new_unique()])
726                .priority_fee(1000)
727                .compute_unit_limit(200_000)
728                .instruction(CompiledInstruction {
729                    program_id_index: 1,
730                    accounts: vec![0],
731                    data: vec![1, 2, 3, 4],
732                })
733                .build()
734                .unwrap(),
735            // Multiple instructions
736            v1::MessageBuilder::new()
737                .required_signatures(2)
738                .lifetime_specifier(Hash::new_unique())
739                .accounts(vec![
740                    Address::new_unique(),
741                    Address::new_unique(),
742                    Address::new_unique(),
743                ])
744                .heap_size(65536)
745                .instructions(vec![
746                    CompiledInstruction {
747                        program_id_index: 2,
748                        accounts: vec![0, 1],
749                        data: vec![0xAA, 0xBB],
750                    },
751                    CompiledInstruction {
752                        program_id_index: 2,
753                        accounts: vec![1],
754                        data: vec![0xCC],
755                    },
756                ])
757                .build()
758                .unwrap(),
759        ];
760
761        for message in test_messages {
762            let versioned = VersionedMessage::V1(message.clone());
763
764            // Wincode roundtrip
765            let bytes = wincode::serialize(&versioned).expect("Wincode serialize failed");
766            let deserialized: VersionedMessage =
767                wincode::deserialize(&bytes).expect("Wincode deserialize failed");
768
769            match deserialized {
770                VersionedMessage::V1(parsed) => assert_eq!(parsed, message),
771                _ => panic!("Expected V1 message"),
772            }
773        }
774    }
775}