Skip to main content

solana_transaction/versioned/
mod.rs

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