Skip to main content

solana_transaction/versioned/
mod.rs

1//! Defines a transaction which supports multiple versions of messages.
2
3use {
4    crate::Transaction,
5    solana_message::{inline_nonce::is_advance_nonce_instruction_data, VersionedMessage},
6    solana_sanitize::SanitizeError,
7    solana_sdk_ids::system_program,
8    solana_signature::Signature,
9    std::cmp::Ordering,
10};
11#[cfg(feature = "wincode")]
12use {
13    core::{mem::MaybeUninit, ptr::copy_nonoverlapping},
14    solana_message::v1::SIGNATURE_SIZE,
15    solana_message::MESSAGE_VERSION_PREFIX,
16    solana_signer::{signers::Signers, SignerError},
17    wincode::{
18        config::Config,
19        containers,
20        io::{Reader, Writer},
21        len::ShortU16,
22        ReadError, ReadResult, SchemaRead, SchemaWrite, UninitBuilder, WriteResult,
23    },
24};
25#[cfg(feature = "serde")]
26use {
27    serde_derive::{Deserialize, Serialize},
28    solana_short_vec as short_vec,
29};
30
31pub mod sanitized;
32
33/// Type that serializes to the string "legacy"
34#[cfg_attr(
35    feature = "serde",
36    derive(Deserialize, Serialize),
37    serde(rename_all = "camelCase")
38)]
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum Legacy {
41    Legacy,
42}
43
44#[cfg_attr(
45    feature = "serde",
46    derive(Deserialize, Serialize),
47    serde(rename_all = "camelCase", untagged)
48)]
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum TransactionVersion {
51    Legacy(Legacy),
52    Number(u8),
53}
54
55impl TransactionVersion {
56    pub const LEGACY: Self = Self::Legacy(Legacy::Legacy);
57}
58
59// NOTE: Serialization-related changes must be paired with the direct read at sigverify.
60/// An atomic transaction
61#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))]
62#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
63#[cfg_attr(feature = "wincode", derive(UninitBuilder))]
64#[derive(Debug, PartialEq, Default, Eq, Clone)]
65pub struct VersionedTransaction {
66    /// List of signatures
67    #[cfg_attr(feature = "serde", serde(with = "short_vec"))]
68    #[cfg_attr(
69        feature = "wincode",
70        wincode(with = "containers::Vec<Signature, ShortU16>")
71    )]
72    pub signatures: Vec<Signature>,
73    /// Message to sign.
74    pub message: VersionedMessage,
75}
76
77impl From<Transaction> for VersionedTransaction {
78    fn from(transaction: Transaction) -> Self {
79        Self {
80            signatures: transaction.signatures,
81            message: VersionedMessage::Legacy(transaction.message),
82        }
83    }
84}
85
86impl VersionedTransaction {
87    /// Signs a versioned message and if successful, returns a signed
88    /// transaction.
89    #[cfg(feature = "wincode")]
90    pub fn try_new<T: Signers + ?Sized>(
91        message: VersionedMessage,
92        keypairs: &T,
93    ) -> std::result::Result<Self, SignerError> {
94        let static_account_keys = message.static_account_keys();
95        if static_account_keys.len() < message.header().num_required_signatures as usize {
96            return Err(SignerError::InvalidInput("invalid message".to_string()));
97        }
98
99        let signer_keys = keypairs.try_pubkeys()?;
100        let expected_signer_keys =
101            &static_account_keys[0..message.header().num_required_signatures as usize];
102
103        match signer_keys.len().cmp(&expected_signer_keys.len()) {
104            Ordering::Greater => Err(SignerError::TooManySigners),
105            Ordering::Less => Err(SignerError::NotEnoughSigners),
106            Ordering::Equal => Ok(()),
107        }?;
108
109        let message_data = message.serialize();
110        let signature_indexes: Vec<usize> = expected_signer_keys
111            .iter()
112            .map(|signer_key| {
113                signer_keys
114                    .iter()
115                    .position(|key| key == signer_key)
116                    .ok_or(SignerError::KeypairPubkeyMismatch)
117            })
118            .collect::<std::result::Result<_, SignerError>>()?;
119
120        let unordered_signatures = keypairs.try_sign_message(&message_data)?;
121        let signatures: Vec<Signature> = signature_indexes
122            .into_iter()
123            .map(|index| {
124                unordered_signatures
125                    .get(index)
126                    .copied()
127                    .ok_or_else(|| SignerError::InvalidInput("invalid keypairs".to_string()))
128            })
129            .collect::<std::result::Result<_, SignerError>>()?;
130
131        Ok(Self {
132            signatures,
133            message,
134        })
135    }
136
137    pub fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
138        self.message.sanitize()?;
139        self.sanitize_signatures()?;
140        Ok(())
141    }
142
143    pub(crate) fn sanitize_signatures(&self) -> std::result::Result<(), SanitizeError> {
144        Self::sanitize_signatures_inner(
145            usize::from(self.message.header().num_required_signatures),
146            self.message.static_account_keys().len(),
147            self.signatures.len(),
148        )
149    }
150
151    pub(crate) fn sanitize_signatures_inner(
152        num_required_signatures: usize,
153        num_static_account_keys: usize,
154        num_signatures: usize,
155    ) -> std::result::Result<(), SanitizeError> {
156        match num_required_signatures.cmp(&num_signatures) {
157            Ordering::Greater => Err(SanitizeError::IndexOutOfBounds),
158            Ordering::Less => Err(SanitizeError::InvalidValue),
159            Ordering::Equal => Ok(()),
160        }?;
161
162        // Signatures are verified before message keys are loaded so all signers
163        // must correspond to static account keys.
164        if num_signatures > num_static_account_keys {
165            return Err(SanitizeError::IndexOutOfBounds);
166        }
167
168        Ok(())
169    }
170
171    /// Returns the version of the transaction
172    pub fn version(&self) -> TransactionVersion {
173        match self.message {
174            VersionedMessage::Legacy(_) => TransactionVersion::LEGACY,
175            VersionedMessage::V0(_) => TransactionVersion::Number(0),
176            VersionedMessage::V1(_) => TransactionVersion::Number(1),
177        }
178    }
179
180    /// Returns a legacy transaction if the transaction message is legacy.
181    pub fn into_legacy_transaction(self) -> Option<Transaction> {
182        match self.message {
183            VersionedMessage::Legacy(message) => Some(Transaction {
184                signatures: self.signatures,
185                message,
186            }),
187            _ => None,
188        }
189    }
190
191    #[cfg(feature = "verify")]
192    /// Verify the transaction and hash its message
193    pub fn verify_and_hash_message(
194        &self,
195    ) -> solana_transaction_error::TransactionResult<solana_hash::Hash> {
196        let message_bytes = self.message.serialize();
197        if !self
198            ._verify_with_results(&message_bytes)
199            .iter()
200            .all(|verify_result| *verify_result)
201        {
202            Err(solana_transaction_error::TransactionError::SignatureFailure)
203        } else {
204            Ok(VersionedMessage::hash_raw_message(&message_bytes))
205        }
206    }
207
208    #[cfg(feature = "verify")]
209    /// Verify the transaction and return a list of verification results
210    pub fn verify_with_results(&self) -> Vec<bool> {
211        let message_bytes = self.message.serialize();
212        self._verify_with_results(&message_bytes)
213    }
214
215    #[cfg(feature = "verify")]
216    fn _verify_with_results(&self, message_bytes: &[u8]) -> Vec<bool> {
217        self.signatures
218            .iter()
219            .zip(self.message.static_account_keys().iter())
220            .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), message_bytes))
221            .collect()
222    }
223
224    /// Returns true if transaction begins with an advance nonce instruction.
225    pub fn uses_durable_nonce(&self) -> bool {
226        let message = &self.message;
227        message
228            .instructions()
229            .get(crate::NONCED_TX_MARKER_IX_INDEX as usize)
230            .filter(|instruction| {
231                // Is system program
232                matches!(
233                    message.static_account_keys().get(instruction.program_id_index as usize),
234                    Some(program_id) if system_program::check_id(program_id)
235                ) && is_advance_nonce_instruction_data(&instruction.data)
236            })
237            .is_some()
238    }
239}
240
241#[cfg(feature = "wincode")]
242unsafe impl<C: Config> SchemaWrite<C> for VersionedTransaction {
243    type Src = Self;
244
245    #[allow(clippy::arithmetic_side_effects)]
246    #[inline]
247    fn size_of(src: &Self::Src) -> WriteResult<usize> {
248        match src.message {
249            VersionedMessage::Legacy(_) | VersionedMessage::V0(_) => {
250                Ok(
251                    <containers::Vec<Signature, ShortU16> as SchemaWrite<C>>::size_of(
252                        &src.signatures,
253                    )? + <VersionedMessage as SchemaWrite<C>>::size_of(&src.message)?,
254                )
255            }
256            VersionedMessage::V1(_) => Ok(
257                // V1 transasction signatures are written as a fixed length array
258                // without a length prefix.
259                <VersionedMessage as SchemaWrite<C>>::size_of(&src.message)?
260                    + src.signatures.len() * SIGNATURE_SIZE,
261            ),
262        }
263    }
264
265    #[inline]
266    fn write(mut writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
267        match src.message {
268            VersionedMessage::Legacy(_) | VersionedMessage::V0(_) => {
269                // `signatures` are written with `ShortU16Len` length prefix.
270                <containers::Vec<Signature, ShortU16> as SchemaWrite<C>>::write(
271                    &mut writer,
272                    &src.signatures,
273                )?;
274                <VersionedMessage as SchemaWrite<C>>::write(writer, &src.message)
275            }
276            VersionedMessage::V1(_) => {
277                <VersionedMessage as SchemaWrite<C>>::write(&mut writer, &src.message)?;
278                unsafe {
279                    writer
280                        .write_slice_t(&src.signatures)
281                        .map_err(wincode::WriteError::Io)
282                }
283            }
284        }
285    }
286}
287
288#[cfg(feature = "wincode")]
289unsafe impl<'de, C: Config> SchemaRead<'de, C> for VersionedTransaction {
290    type Dst = Self;
291
292    #[inline]
293    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
294        // Peek the discriminator to decide how to read the transaction data.
295        //
296        // - For `Legacy` and `V0` messages, the first byte is part of the `short_vec` length
297        //   prefix for the `signatures` field. Since `signatures < 128` is always true, if
298        //   the top bit is `0`, we expect the message to be either `Legacy` or `V0`.
299        //
300        // - For `V1` messages, the first byte is the message version byte, which is always
301        //   `> 128` and the top bit is always `1`.
302
303        use solana_message::v1::V1_PREFIX;
304        let discriminator = reader.peek()?;
305        let mut builder = VersionedTransactionUninitBuilder::<C>::from_maybe_uninit_mut(dst);
306
307        if discriminator & MESSAGE_VERSION_PREFIX == 0 {
308            // Legacy or V0 transaction
309
310            builder.read_signatures(&mut reader)?;
311            builder.read_message(reader)?;
312
313            // SAFETY: The transaction is fully initialized at this point since
314            // both `signatures` and `message` have been read.
315            let message = unsafe { builder.uninit_message_mut().assume_init_ref() };
316
317            // validate that we got either a legacy or V0 message
318            if !matches!(
319                message,
320                VersionedMessage::Legacy(_) | VersionedMessage::V0(_)
321            ) {
322                return Err(ReadError::Custom("invalid message version"));
323            }
324
325            builder.finish();
326        } else if *discriminator == V1_PREFIX {
327            // V1 transaction
328
329            builder.read_message(&mut reader)?;
330
331            // SAFETY: message is initialized by the above read.
332            let message = unsafe { builder.uninit_message_mut().assume_init_ref() };
333
334            // validate that we got a V1 message
335            if !matches!(message, VersionedMessage::V1(_)) {
336                return Err(ReadError::Custom("invalid message version"));
337            }
338
339            let expected_signatures_len = message.header().num_required_signatures as usize;
340
341            let bytes_to_read = expected_signatures_len.saturating_mul(SIGNATURE_SIZE);
342            let bytes = reader.fill_exact(bytes_to_read)?;
343            let mut signatures = Vec::with_capacity(expected_signatures_len);
344
345            // SAFETY: signatures vector is allocated with enough capacity to hold
346            // `expected_signatures_len` signatures and `bytes` contains exactly that
347            // many signatures read from the reader.
348            unsafe {
349                let signatures_ptr = signatures.as_mut_ptr();
350                copy_nonoverlapping(
351                    bytes.as_ptr() as *const Signature,
352                    signatures_ptr,
353                    expected_signatures_len,
354                );
355                signatures.set_len(expected_signatures_len);
356                // Advance the reader by the number of bytes we just consumed
357                // for the signatures.
358                reader.consume_unchecked(bytes_to_read);
359            }
360
361            builder.write_signatures(signatures);
362            builder.finish();
363        } else {
364            return Err(ReadError::Custom("invalid transaction discriminator"));
365        }
366
367        Ok(())
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use {
374        super::*,
375        solana_address::{Address, ADDRESS_BYTES},
376        solana_hash::Hash,
377        solana_instruction::{AccountMeta, Instruction},
378        solana_keypair::Keypair,
379        solana_message::{
380            compiled_instruction::CompiledInstruction,
381            v0::Message as MessageV0,
382            v1::{
383                InstructionHeader, Message, TransactionConfig, FIXED_HEADER_SIZE,
384                MAX_TRANSACTION_SIZE, SIGNATURE_SIZE,
385            },
386            Message as LegacyMessage, MessageHeader,
387        },
388        solana_pubkey::Pubkey,
389        solana_signer::Signer,
390        solana_system_interface::instruction as system_instruction,
391        test_case::test_case,
392    };
393
394    #[test]
395    fn test_try_new() {
396        let keypair0 = Keypair::new();
397        let keypair1 = Keypair::new();
398        let keypair2 = Keypair::new();
399
400        let message = VersionedMessage::Legacy(LegacyMessage::new(
401            &[Instruction::new_with_bytes(
402                Pubkey::new_unique(),
403                &[],
404                vec![
405                    AccountMeta::new_readonly(keypair1.pubkey(), true),
406                    AccountMeta::new_readonly(keypair2.pubkey(), false),
407                ],
408            )],
409            Some(&keypair0.pubkey()),
410        ));
411
412        assert_eq!(
413            VersionedTransaction::try_new(message.clone(), &[&keypair0]),
414            Err(SignerError::NotEnoughSigners)
415        );
416
417        assert_eq!(
418            VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair0]),
419            Err(SignerError::KeypairPubkeyMismatch)
420        );
421
422        assert_eq!(
423            VersionedTransaction::try_new(message.clone(), &[&keypair1, &keypair2]),
424            Err(SignerError::KeypairPubkeyMismatch)
425        );
426
427        match VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair1]) {
428            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
429            Err(err) => assert_eq!(Some(err), None),
430        }
431
432        match VersionedTransaction::try_new(message, &[&keypair1, &keypair0]) {
433            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
434            Err(err) => assert_eq!(Some(err), None),
435        }
436    }
437
438    fn nonced_transfer_tx() -> (Pubkey, Pubkey, VersionedTransaction) {
439        let from_keypair = Keypair::new();
440        let from_pubkey = from_keypair.pubkey();
441        let nonce_keypair = Keypair::new();
442        let nonce_pubkey = nonce_keypair.pubkey();
443        let instructions = [
444            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
445            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
446        ];
447        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
448        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
449        (from_pubkey, nonce_pubkey, tx.into())
450    }
451
452    #[test]
453    fn tx_uses_nonce_ok() {
454        let (_, _, tx) = nonced_transfer_tx();
455        assert!(tx.uses_durable_nonce());
456    }
457
458    #[test]
459    fn tx_uses_nonce_empty_ix_fail() {
460        let tx = VersionedTransaction {
461            message: VersionedMessage::V0(MessageV0::default()),
462            signatures: vec![],
463        };
464        assert!(!tx.uses_durable_nonce());
465    }
466
467    #[test]
468    fn tx_uses_nonce_bad_prog_id_idx_fail() {
469        let (_, _, mut tx) = nonced_transfer_tx();
470        match &mut tx.message {
471            VersionedMessage::Legacy(message) => {
472                message.instructions.get_mut(0).unwrap().program_id_index = 255u8;
473            }
474            _ => unreachable!(),
475        };
476        assert!(!tx.uses_durable_nonce());
477    }
478
479    #[test]
480    fn tx_uses_nonce_first_prog_id_not_nonce_fail() {
481        let from_keypair = Keypair::new();
482        let from_pubkey = from_keypair.pubkey();
483        let nonce_keypair = Keypair::new();
484        let nonce_pubkey = nonce_keypair.pubkey();
485        let instructions = [
486            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
487            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
488        ];
489        let message = LegacyMessage::new(&instructions, Some(&from_pubkey));
490        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
491        let tx = VersionedTransaction::from(tx);
492        assert!(!tx.uses_durable_nonce());
493    }
494
495    #[test]
496    fn tx_uses_nonce_wrong_first_nonce_ix_fail() {
497        let from_keypair = Keypair::new();
498        let from_pubkey = from_keypair.pubkey();
499        let nonce_keypair = Keypair::new();
500        let nonce_pubkey = nonce_keypair.pubkey();
501        let instructions = [
502            system_instruction::withdraw_nonce_account(
503                &nonce_pubkey,
504                &nonce_pubkey,
505                &from_pubkey,
506                42,
507            ),
508            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
509        ];
510        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
511        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
512        let tx = VersionedTransaction::from(tx);
513        assert!(!tx.uses_durable_nonce());
514    }
515
516    #[test]
517    fn test_sanitize_signatures_inner() {
518        assert_eq!(
519            VersionedTransaction::sanitize_signatures_inner(1, 1, 0),
520            Err(SanitizeError::IndexOutOfBounds)
521        );
522        assert_eq!(
523            VersionedTransaction::sanitize_signatures_inner(1, 1, 2),
524            Err(SanitizeError::InvalidValue)
525        );
526        assert_eq!(
527            VersionedTransaction::sanitize_signatures_inner(2, 1, 2),
528            Err(SanitizeError::IndexOutOfBounds)
529        );
530        assert_eq!(
531            VersionedTransaction::sanitize_signatures_inner(1, 1, 1),
532            Ok(())
533        );
534    }
535
536    #[test]
537    fn versioned_transaction_wincode_bincode_roundtrip() {
538        use {
539            super::*,
540            proptest::prelude::*,
541            solana_address::{Address, ADDRESS_BYTES},
542            solana_hash::{Hash, HASH_BYTES},
543            solana_message::{
544                compiled_instruction::CompiledInstruction,
545                v0::{self, MessageAddressTableLookup},
546                Message as LegacyMessage, MessageHeader,
547            },
548            solana_signature::SIGNATURE_BYTES,
549        };
550
551        // Bincode version of VersionedTransaction for cross-checking serialization
552        // with wincode. This only applies to legacy/v0 transactions since v1
553        // transaction format is not compatible with bincode.
554        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
555        #[derive(Debug, PartialEq, Default, Eq, Clone)]
556        struct BincodeVersionedTransaction {
557            /// List of signatures
558            #[cfg_attr(feature = "serde", serde(with = "short_vec"))]
559            pub signatures: Vec<Signature>,
560            /// Message to sign.
561            pub message: VersionedMessage,
562        }
563
564        fn strat_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
565            proptest::collection::vec(any::<u8>(), 0..=max_len)
566        }
567
568        fn strat_signature() -> impl Strategy<Value = Signature> {
569            any::<[u8; SIGNATURE_BYTES]>().prop_map(Signature::from)
570        }
571
572        fn strat_address() -> impl Strategy<Value = Address> {
573            any::<[u8; ADDRESS_BYTES]>().prop_map(Address::new_from_array)
574        }
575
576        fn strat_hash() -> impl Strategy<Value = Hash> {
577            any::<[u8; HASH_BYTES]>().prop_map(Hash::new_from_array)
578        }
579
580        fn strat_message_header() -> impl Strategy<Value = MessageHeader> {
581            (0u8..128, any::<u8>(), any::<u8>()).prop_map(|(a, b, c)| MessageHeader {
582                num_required_signatures: a,
583                num_readonly_signed_accounts: b,
584                num_readonly_unsigned_accounts: c,
585            })
586        }
587
588        fn strat_compiled_instruction() -> impl Strategy<Value = CompiledInstruction> {
589            (any::<u8>(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
590                |(program_id_index, accounts, data)| {
591                    CompiledInstruction::new_from_raw_parts(program_id_index, accounts, data)
592                },
593            )
594        }
595
596        fn strat_address_table_lookup() -> impl Strategy<Value = MessageAddressTableLookup> {
597            (strat_address(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
598                |(account_key, writable_indexes, readonly_indexes)| MessageAddressTableLookup {
599                    account_key,
600                    writable_indexes,
601                    readonly_indexes,
602                },
603            )
604        }
605
606        fn strat_legacy_message() -> impl Strategy<Value = LegacyMessage> {
607            (
608                strat_message_header(),
609                proptest::collection::vec(strat_address(), 0..=8),
610                strat_hash(),
611                proptest::collection::vec(strat_compiled_instruction(), 0..=8),
612            )
613                .prop_map(|(header, account_keys, recent_blockhash, instructions)| {
614                    LegacyMessage {
615                        header,
616                        account_keys,
617                        recent_blockhash,
618                        instructions,
619                    }
620                })
621        }
622
623        fn strat_v0_message() -> impl Strategy<Value = v0::Message> {
624            (
625                strat_message_header(),
626                proptest::collection::vec(strat_address(), 0..=8),
627                strat_hash(),
628                proptest::collection::vec(strat_compiled_instruction(), 0..=4),
629                proptest::collection::vec(strat_address_table_lookup(), 0..=4),
630            )
631                .prop_map(
632                    |(
633                        header,
634                        account_keys,
635                        recent_blockhash,
636                        instructions,
637                        address_table_lookups,
638                    )| {
639                        v0::Message {
640                            header,
641                            account_keys,
642                            recent_blockhash,
643                            instructions,
644                            address_table_lookups,
645                        }
646                    },
647                )
648        }
649
650        fn strat_versioned_message() -> impl Strategy<Value = VersionedMessage> {
651            prop_oneof![
652                strat_legacy_message().prop_map(VersionedMessage::Legacy),
653                strat_v0_message().prop_map(VersionedMessage::V0),
654            ]
655        }
656
657        fn strat_versioned_transaction(
658        ) -> impl Strategy<Value = (VersionedTransaction, BincodeVersionedTransaction)> {
659            (
660                proptest::collection::vec(strat_signature(), 0..=8),
661                strat_versioned_message(),
662            )
663                .prop_map(|(signatures, message)| {
664                    (
665                        VersionedTransaction {
666                            message: message.clone(),
667                            signatures: signatures.clone(),
668                        },
669                        BincodeVersionedTransaction {
670                            message: message.clone(),
671                            signatures: signatures.clone(),
672                        },
673                    )
674                })
675        }
676
677        proptest!(|(tx in strat_versioned_transaction())| {
678            let wincode_serialized = wincode::serialize(&tx.0).unwrap();
679            let bincode_serialized = bincode::serialize(&tx.1).unwrap();
680
681            assert_eq!(bincode_serialized, wincode_serialized);
682
683            let bincode_deserialized: BincodeVersionedTransaction = bincode::deserialize(&bincode_serialized).unwrap();
684            let wincode_deserialized: VersionedTransaction = wincode::deserialize(&wincode_serialized).unwrap();
685
686            assert_eq!(&bincode_deserialized.message, &wincode_deserialized.message);
687            assert_eq!(&bincode_deserialized.signatures, &wincode_deserialized.signatures);
688
689            assert_eq!(wincode_deserialized, tx.0);
690        });
691    }
692
693    #[test_case(0 ; "at max size")]
694    #[test_case(1 ; "over by one")]
695    #[allow(clippy::arithmetic_side_effects)]
696    fn v1_transaction_serialization(delta: usize) {
697        // Calculate exact max data size for a transaction at the limit:
698        // - 1 signature
699        // - Fixed header (version + MessageHeader + config mask + lifetime + num_ix + num_addr)
700        // - 2 addresses
701        // - No config values (mask = 0)
702        // - 1 instruction header
703        // - 1 account index in instruction
704        const NUM_SIGNATURES: usize = 1;
705        const NUM_ADDRESSES: usize = 2;
706        const NUM_INSTRUCTION_ACCOUNTS: usize = 1;
707
708        let overhead = 1 // version byte
709            + (NUM_SIGNATURES * SIGNATURE_SIZE)
710            + FIXED_HEADER_SIZE
711            + (NUM_ADDRESSES * ADDRESS_BYTES)
712            + size_of::<InstructionHeader>()
713            + NUM_INSTRUCTION_ACCOUNTS;
714
715        // adds `delta` bytes to the instruction data to test both at max size
716        // and over by one byte scenarios.
717        let max_data_size = MAX_TRANSACTION_SIZE - overhead + delta;
718        let data = vec![0u8; max_data_size];
719
720        let message = Message {
721            header: MessageHeader {
722                num_required_signatures: NUM_SIGNATURES as u8,
723                num_readonly_signed_accounts: 0,
724                num_readonly_unsigned_accounts: 0,
725            },
726            config: TransactionConfig::default(),
727            account_keys: vec![Address::new_unique(), Address::new_unique()],
728            lifetime_specifier: Hash::new_unique(),
729            instructions: vec![CompiledInstruction {
730                program_id_index: 1,
731                accounts: vec![0],
732                data,
733            }],
734        };
735
736        let v1_tx = VersionedTransaction {
737            message: VersionedMessage::V1(message),
738            signatures: vec![Signature::default()],
739        };
740
741        let serialized = wincode::serialize(&v1_tx).unwrap();
742
743        match delta {
744            0 => assert_eq!(
745                serialized.len(),
746                MAX_TRANSACTION_SIZE,
747                "Transaction should be exactly at max size"
748            ),
749            d => assert_eq!(
750                serialized.len(),
751                MAX_TRANSACTION_SIZE + d,
752                "Transaction should be over by {d} byte(s)"
753            ),
754        }
755
756        let deserialized = wincode::deserialize(&serialized).unwrap();
757
758        assert_eq!(
759            v1_tx, deserialized,
760            "Deserialized payload should match original"
761        );
762    }
763
764    #[test]
765    fn test_v1_message_in_legacy_transaction() {
766        #[rustfmt::skip]
767        let malformed_input: &[u8] = &[
768            0x00,                   // 0 signatures via ShortU16 -> takes Legacy/V0 path
769            0x81,                   // V1 message prefix
770            // V1 LegacyHeader (3 bytes)
771            0x01,                   // num_required_signatures = 1
772            0x00,                   // num_readonly_signed_accounts = 0
773            0x00,                   // num_readonly_unsigned_accounts = 0
774            // TransactionConfigMask (4 bytes, little-endian)
775            0x00, 0x00, 0x00, 0x00,
776            // LifetimeSpecifier / blockhash (32 bytes)
777            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
778            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
779            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
780            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
781            // NumInstructions (1 byte)
782            0x00,
783            // NumAddresses (1 byte)
784            0x01,
785            // 1 address (32 bytes)
786            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
787            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
788            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
789            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
790        ];
791
792        let result: Result<VersionedTransaction, _> = wincode::deserialize(malformed_input);
793
794        if let Err(wincode::ReadError::Custom(msg)) = result {
795            assert_eq!(msg, "invalid message version");
796        } else {
797            panic!("Deserialization should not succeed with a V1 message in Legacy/V0 format")
798        }
799    }
800}