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,
14    solana_message::{v1::SIGNATURE_SIZE, MESSAGE_VERSION_PREFIX},
15    solana_short_vec::ShortU16,
16    solana_signer::{signers::Signers, SignerError},
17    wincode::{
18        config::Config,
19        containers, context,
20        io::{Reader, Writer},
21        ReadError, ReadResult, SchemaRead, SchemaReadContext, SchemaWrite, UninitBuilder,
22        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.take_byte()?;
305
306        if discriminator & MESSAGE_VERSION_PREFIX == 0 {
307            // Legacy or V0 transaction
308
309            let signatures = <Vec<Signature> as SchemaReadContext<C, _>>::get_with_context(
310                // Here `discriminator < 0x80`, so it is a canonical one-byte `ShortU16`.
311                context::Len(discriminator as usize),
312                reader.by_ref(),
313            )?;
314            let message = <VersionedMessage as SchemaRead<C>>::get(reader)?;
315
316            // validate that we got either a legacy or V0 message
317            if !matches!(
318                message,
319                VersionedMessage::Legacy(_) | VersionedMessage::V0(_)
320            ) {
321                return Err(ReadError::Custom("invalid message version"));
322            }
323
324            dst.write(Self {
325                signatures,
326                message,
327            });
328        } else if discriminator == V1_PREFIX {
329            // V1 transaction
330
331            let message = <VersionedMessage as SchemaReadContext<C, _>>::get_with_context(
332                // `discriminator` is the already-consumed first byte of the serialized
333                // `VersionedMessage`, so pass it as read context instead of reading it again.
334                discriminator,
335                reader.by_ref(),
336            )?;
337
338            // validate that we got a V1 message
339            if !matches!(message, VersionedMessage::V1(_)) {
340                return Err(ReadError::Custom("invalid message version"));
341            }
342
343            let num_signatures = message.header().num_required_signatures as usize;
344            let signatures = <Vec<Signature> as SchemaReadContext<C, _>>::get_with_context(
345                context::Len(num_signatures),
346                reader,
347            )?;
348
349            dst.write(Self {
350                signatures,
351                message,
352            });
353        } else {
354            return Err(ReadError::Custom("invalid transaction discriminator"));
355        }
356
357        Ok(())
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use {
364        super::*,
365        solana_address::{Address, ADDRESS_BYTES},
366        solana_hash::Hash,
367        solana_instruction::{AccountMeta, Instruction},
368        solana_keypair::Keypair,
369        solana_message::{
370            compiled_instruction::CompiledInstruction,
371            v0::Message as MessageV0,
372            v1::{
373                InstructionHeader, Message, TransactionConfig, FIXED_HEADER_SIZE,
374                MAX_TRANSACTION_SIZE, SIGNATURE_SIZE,
375            },
376            Message as LegacyMessage, MessageHeader,
377        },
378        solana_pubkey::Pubkey,
379        solana_signer::Signer,
380        solana_system_interface::instruction as system_instruction,
381        test_case::test_case,
382    };
383
384    #[test]
385    fn test_try_new() {
386        let keypair0 = Keypair::new();
387        let keypair1 = Keypair::new();
388        let keypair2 = Keypair::new();
389
390        let message = VersionedMessage::Legacy(LegacyMessage::new(
391            &[Instruction::new_with_bytes(
392                Pubkey::new_unique(),
393                &[],
394                vec![
395                    AccountMeta::new_readonly(keypair1.pubkey(), true),
396                    AccountMeta::new_readonly(keypair2.pubkey(), false),
397                ],
398            )],
399            Some(&keypair0.pubkey()),
400        ));
401
402        assert_eq!(
403            VersionedTransaction::try_new(message.clone(), &[&keypair0]),
404            Err(SignerError::NotEnoughSigners)
405        );
406
407        assert_eq!(
408            VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair0]),
409            Err(SignerError::KeypairPubkeyMismatch)
410        );
411
412        assert_eq!(
413            VersionedTransaction::try_new(message.clone(), &[&keypair1, &keypair2]),
414            Err(SignerError::KeypairPubkeyMismatch)
415        );
416
417        match VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair1]) {
418            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
419            Err(err) => assert_eq!(Some(err), None),
420        }
421
422        match VersionedTransaction::try_new(message, &[&keypair1, &keypair0]) {
423            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
424            Err(err) => assert_eq!(Some(err), None),
425        }
426    }
427
428    fn nonced_transfer_tx() -> (Pubkey, Pubkey, VersionedTransaction) {
429        let from_keypair = Keypair::new();
430        let from_pubkey = from_keypair.pubkey();
431        let nonce_keypair = Keypair::new();
432        let nonce_pubkey = nonce_keypair.pubkey();
433        let instructions = [
434            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
435            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
436        ];
437        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
438        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
439        (from_pubkey, nonce_pubkey, tx.into())
440    }
441
442    #[test]
443    fn tx_uses_nonce_ok() {
444        let (_, _, tx) = nonced_transfer_tx();
445        assert!(tx.uses_durable_nonce());
446    }
447
448    #[test]
449    fn tx_uses_nonce_empty_ix_fail() {
450        let tx = VersionedTransaction {
451            message: VersionedMessage::V0(MessageV0::default()),
452            signatures: vec![],
453        };
454        assert!(!tx.uses_durable_nonce());
455    }
456
457    #[test]
458    fn tx_uses_nonce_bad_prog_id_idx_fail() {
459        let (_, _, mut tx) = nonced_transfer_tx();
460        match &mut tx.message {
461            VersionedMessage::Legacy(message) => {
462                message.instructions.get_mut(0).unwrap().program_id_index = 255u8;
463            }
464            _ => unreachable!(),
465        };
466        assert!(!tx.uses_durable_nonce());
467    }
468
469    #[test]
470    fn tx_uses_nonce_first_prog_id_not_nonce_fail() {
471        let from_keypair = Keypair::new();
472        let from_pubkey = from_keypair.pubkey();
473        let nonce_keypair = Keypair::new();
474        let nonce_pubkey = nonce_keypair.pubkey();
475        let instructions = [
476            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
477            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
478        ];
479        let message = LegacyMessage::new(&instructions, Some(&from_pubkey));
480        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
481        let tx = VersionedTransaction::from(tx);
482        assert!(!tx.uses_durable_nonce());
483    }
484
485    #[test]
486    fn tx_uses_nonce_wrong_first_nonce_ix_fail() {
487        let from_keypair = Keypair::new();
488        let from_pubkey = from_keypair.pubkey();
489        let nonce_keypair = Keypair::new();
490        let nonce_pubkey = nonce_keypair.pubkey();
491        let instructions = [
492            system_instruction::withdraw_nonce_account(
493                &nonce_pubkey,
494                &nonce_pubkey,
495                &from_pubkey,
496                42,
497            ),
498            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
499        ];
500        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
501        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
502        let tx = VersionedTransaction::from(tx);
503        assert!(!tx.uses_durable_nonce());
504    }
505
506    #[test]
507    fn test_sanitize_signatures_inner() {
508        assert_eq!(
509            VersionedTransaction::sanitize_signatures_inner(1, 1, 0),
510            Err(SanitizeError::IndexOutOfBounds)
511        );
512        assert_eq!(
513            VersionedTransaction::sanitize_signatures_inner(1, 1, 2),
514            Err(SanitizeError::InvalidValue)
515        );
516        assert_eq!(
517            VersionedTransaction::sanitize_signatures_inner(2, 1, 2),
518            Err(SanitizeError::IndexOutOfBounds)
519        );
520        assert_eq!(
521            VersionedTransaction::sanitize_signatures_inner(1, 1, 1),
522            Ok(())
523        );
524    }
525
526    #[test]
527    fn versioned_transaction_wincode_bincode_roundtrip() {
528        use {
529            super::*,
530            proptest::prelude::*,
531            solana_address::{Address, ADDRESS_BYTES},
532            solana_hash::{Hash, HASH_BYTES},
533            solana_message::{
534                compiled_instruction::CompiledInstruction,
535                v0::{self, MessageAddressTableLookup},
536                Message as LegacyMessage, MessageHeader,
537            },
538            solana_signature::SIGNATURE_BYTES,
539        };
540
541        // Bincode version of VersionedTransaction for cross-checking serialization
542        // with wincode. This only applies to legacy/v0 transactions since v1
543        // transaction format is not compatible with bincode.
544        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
545        #[derive(Debug, PartialEq, Default, Eq, Clone)]
546        struct BincodeVersionedTransaction {
547            /// List of signatures
548            #[cfg_attr(feature = "serde", serde(with = "short_vec"))]
549            pub signatures: Vec<Signature>,
550            /// Message to sign.
551            pub message: VersionedMessage,
552        }
553
554        fn strat_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
555            proptest::collection::vec(any::<u8>(), 0..=max_len)
556        }
557
558        fn strat_signature() -> impl Strategy<Value = Signature> {
559            any::<[u8; SIGNATURE_BYTES]>().prop_map(Signature::from)
560        }
561
562        fn strat_address() -> impl Strategy<Value = Address> {
563            any::<[u8; ADDRESS_BYTES]>().prop_map(Address::new_from_array)
564        }
565
566        fn strat_hash() -> impl Strategy<Value = Hash> {
567            any::<[u8; HASH_BYTES]>().prop_map(Hash::new_from_array)
568        }
569
570        fn strat_message_header() -> impl Strategy<Value = MessageHeader> {
571            (0u8..128, any::<u8>(), any::<u8>()).prop_map(|(a, b, c)| MessageHeader {
572                num_required_signatures: a,
573                num_readonly_signed_accounts: b,
574                num_readonly_unsigned_accounts: c,
575            })
576        }
577
578        fn strat_compiled_instruction() -> impl Strategy<Value = CompiledInstruction> {
579            (any::<u8>(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
580                |(program_id_index, accounts, data)| {
581                    CompiledInstruction::new_from_raw_parts(program_id_index, accounts, data)
582                },
583            )
584        }
585
586        fn strat_address_table_lookup() -> impl Strategy<Value = MessageAddressTableLookup> {
587            (strat_address(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
588                |(account_key, writable_indexes, readonly_indexes)| MessageAddressTableLookup {
589                    account_key,
590                    writable_indexes,
591                    readonly_indexes,
592                },
593            )
594        }
595
596        fn strat_legacy_message() -> impl Strategy<Value = LegacyMessage> {
597            (
598                strat_message_header(),
599                proptest::collection::vec(strat_address(), 0..=8),
600                strat_hash(),
601                proptest::collection::vec(strat_compiled_instruction(), 0..=8),
602            )
603                .prop_map(|(header, account_keys, recent_blockhash, instructions)| {
604                    LegacyMessage {
605                        header,
606                        account_keys,
607                        recent_blockhash,
608                        instructions,
609                    }
610                })
611        }
612
613        fn strat_v0_message() -> impl Strategy<Value = v0::Message> {
614            (
615                strat_message_header(),
616                proptest::collection::vec(strat_address(), 0..=8),
617                strat_hash(),
618                proptest::collection::vec(strat_compiled_instruction(), 0..=4),
619                proptest::collection::vec(strat_address_table_lookup(), 0..=4),
620            )
621                .prop_map(
622                    |(
623                        header,
624                        account_keys,
625                        recent_blockhash,
626                        instructions,
627                        address_table_lookups,
628                    )| {
629                        v0::Message {
630                            header,
631                            account_keys,
632                            recent_blockhash,
633                            instructions,
634                            address_table_lookups,
635                        }
636                    },
637                )
638        }
639
640        fn strat_versioned_message() -> impl Strategy<Value = VersionedMessage> {
641            prop_oneof![
642                strat_legacy_message().prop_map(VersionedMessage::Legacy),
643                strat_v0_message().prop_map(VersionedMessage::V0),
644            ]
645        }
646
647        fn strat_versioned_transaction(
648        ) -> impl Strategy<Value = (VersionedTransaction, BincodeVersionedTransaction)> {
649            (
650                proptest::collection::vec(strat_signature(), 0..=8),
651                strat_versioned_message(),
652            )
653                .prop_map(|(signatures, message)| {
654                    (
655                        VersionedTransaction {
656                            message: message.clone(),
657                            signatures: signatures.clone(),
658                        },
659                        BincodeVersionedTransaction {
660                            message: message.clone(),
661                            signatures: signatures.clone(),
662                        },
663                    )
664                })
665        }
666
667        proptest!(|(tx in strat_versioned_transaction())| {
668            let wincode_serialized = wincode::serialize(&tx.0).unwrap();
669            let bincode_serialized = bincode::serialize(&tx.1).unwrap();
670
671            assert_eq!(bincode_serialized, wincode_serialized);
672
673            let bincode_deserialized: BincodeVersionedTransaction = bincode::deserialize(&bincode_serialized).unwrap();
674            let wincode_deserialized: VersionedTransaction = wincode::deserialize(&wincode_serialized).unwrap();
675
676            assert_eq!(&bincode_deserialized.message, &wincode_deserialized.message);
677            assert_eq!(&bincode_deserialized.signatures, &wincode_deserialized.signatures);
678
679            assert_eq!(wincode_deserialized, tx.0);
680        });
681    }
682
683    #[test_case(0 ; "at max size")]
684    #[test_case(1 ; "over by one")]
685    #[allow(clippy::arithmetic_side_effects)]
686    fn v1_transaction_serialization(delta: usize) {
687        // Calculate exact max data size for a transaction at the limit:
688        // - 1 signature
689        // - Fixed header (version + MessageHeader + config mask + lifetime + num_ix + num_addr)
690        // - 2 addresses
691        // - No config values (mask = 0)
692        // - 1 instruction header
693        // - 1 account index in instruction
694        const NUM_SIGNATURES: usize = 1;
695        const NUM_ADDRESSES: usize = 2;
696        const NUM_INSTRUCTION_ACCOUNTS: usize = 1;
697
698        let overhead = 1 // version byte
699            + (NUM_SIGNATURES * SIGNATURE_SIZE)
700            + FIXED_HEADER_SIZE
701            + (NUM_ADDRESSES * ADDRESS_BYTES)
702            + size_of::<InstructionHeader>()
703            + NUM_INSTRUCTION_ACCOUNTS;
704
705        // adds `delta` bytes to the instruction data to test both at max size
706        // and over by one byte scenarios.
707        let max_data_size = MAX_TRANSACTION_SIZE - overhead + delta;
708        let data = vec![0u8; max_data_size];
709
710        let message = Message {
711            header: MessageHeader {
712                num_required_signatures: NUM_SIGNATURES as u8,
713                num_readonly_signed_accounts: 0,
714                num_readonly_unsigned_accounts: 0,
715            },
716            config: TransactionConfig::default(),
717            account_keys: vec![Address::new_unique(), Address::new_unique()],
718            lifetime_specifier: Hash::new_unique(),
719            instructions: vec![CompiledInstruction {
720                program_id_index: 1,
721                accounts: vec![0],
722                data,
723            }],
724        };
725
726        let v1_tx = VersionedTransaction {
727            message: VersionedMessage::V1(message),
728            signatures: vec![Signature::default()],
729        };
730
731        let serialized = wincode::serialize(&v1_tx).unwrap();
732
733        match delta {
734            0 => assert_eq!(
735                serialized.len(),
736                MAX_TRANSACTION_SIZE,
737                "Transaction should be exactly at max size"
738            ),
739            d => assert_eq!(
740                serialized.len(),
741                MAX_TRANSACTION_SIZE + d,
742                "Transaction should be over by {d} byte(s)"
743            ),
744        }
745
746        let deserialized = wincode::deserialize(&serialized).unwrap();
747
748        assert_eq!(
749            v1_tx, deserialized,
750            "Deserialized payload should match original"
751        );
752    }
753
754    #[test]
755    fn test_v1_message_in_legacy_transaction() {
756        #[rustfmt::skip]
757        let malformed_input: &[u8] = &[
758            0x00,                   // 0 signatures via ShortU16 -> takes Legacy/V0 path
759            0x81,                   // V1 message prefix
760            // V1 LegacyHeader (3 bytes)
761            0x01,                   // num_required_signatures = 1
762            0x00,                   // num_readonly_signed_accounts = 0
763            0x00,                   // num_readonly_unsigned_accounts = 0
764            // TransactionConfigMask (4 bytes, little-endian)
765            0x00, 0x00, 0x00, 0x00,
766            // LifetimeSpecifier / blockhash (32 bytes)
767            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
768            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
769            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
770            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
771            // NumInstructions (1 byte)
772            0x00,
773            // NumAddresses (1 byte)
774            0x01,
775            // 1 address (32 bytes)
776            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
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        ];
781
782        let result: Result<VersionedTransaction, _> = wincode::deserialize(malformed_input);
783
784        if let Err(wincode::ReadError::Custom(msg)) = result {
785            assert_eq!(msg, "invalid message version");
786        } else {
787            panic!("Deserialization should not succeed with a V1 message in Legacy/V0 format")
788        }
789    }
790}