Skip to main content

solana_transaction/versioned/
mod.rs

1//! Defines a transaction which supports multiple versions of messages.
2
3#[cfg(feature = "bincode")]
4use solana_signer::{signers::Signers, SignerError};
5#[cfg(feature = "wincode")]
6use wincode::{containers, len::ShortU16Len, SchemaRead, SchemaWrite};
7use {
8    crate::Transaction,
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    std::cmp::Ordering,
14};
15#[cfg(feature = "serde")]
16use {
17    serde_derive::{Deserialize, Serialize},
18    solana_short_vec as short_vec,
19};
20
21pub mod sanitized;
22
23/// Type that serializes to the string "legacy"
24#[cfg_attr(
25    feature = "serde",
26    derive(Deserialize, Serialize),
27    serde(rename_all = "camelCase")
28)]
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Legacy {
31    Legacy,
32}
33
34#[cfg_attr(
35    feature = "serde",
36    derive(Deserialize, Serialize),
37    serde(rename_all = "camelCase", untagged)
38)]
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum TransactionVersion {
41    Legacy(Legacy),
42    Number(u8),
43}
44
45impl TransactionVersion {
46    pub const LEGACY: Self = Self::Legacy(Legacy::Legacy);
47}
48
49// NOTE: Serialization-related changes must be paired with the direct read at sigverify.
50/// An atomic transaction
51#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))]
52#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
53#[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
54#[derive(Debug, PartialEq, Default, Eq, Clone)]
55pub struct VersionedTransaction {
56    /// List of signatures
57    #[cfg_attr(feature = "serde", serde(with = "short_vec"))]
58    #[cfg_attr(feature = "wincode", wincode(with = "containers::Vec<_, ShortU16Len>"))]
59    pub signatures: Vec<Signature>,
60    /// Message to sign.
61    pub message: VersionedMessage,
62}
63
64impl From<Transaction> for VersionedTransaction {
65    fn from(transaction: Transaction) -> Self {
66        Self {
67            signatures: transaction.signatures,
68            message: VersionedMessage::Legacy(transaction.message),
69        }
70    }
71}
72
73impl VersionedTransaction {
74    /// Signs a versioned message and if successful, returns a signed
75    /// transaction.
76    #[cfg(feature = "bincode")]
77    pub fn try_new<T: Signers + ?Sized>(
78        message: VersionedMessage,
79        keypairs: &T,
80    ) -> std::result::Result<Self, SignerError> {
81        let static_account_keys = message.static_account_keys();
82        if static_account_keys.len() < message.header().num_required_signatures as usize {
83            return Err(SignerError::InvalidInput("invalid message".to_string()));
84        }
85
86        let signer_keys = keypairs.try_pubkeys()?;
87        let expected_signer_keys =
88            &static_account_keys[0..message.header().num_required_signatures as usize];
89
90        match signer_keys.len().cmp(&expected_signer_keys.len()) {
91            Ordering::Greater => Err(SignerError::TooManySigners),
92            Ordering::Less => Err(SignerError::NotEnoughSigners),
93            Ordering::Equal => Ok(()),
94        }?;
95
96        let message_data = message.serialize();
97        let signature_indexes: Vec<usize> = expected_signer_keys
98            .iter()
99            .map(|signer_key| {
100                signer_keys
101                    .iter()
102                    .position(|key| key == signer_key)
103                    .ok_or(SignerError::KeypairPubkeyMismatch)
104            })
105            .collect::<std::result::Result<_, SignerError>>()?;
106
107        let unordered_signatures = keypairs.try_sign_message(&message_data)?;
108        let signatures: Vec<Signature> = signature_indexes
109            .into_iter()
110            .map(|index| {
111                unordered_signatures
112                    .get(index)
113                    .copied()
114                    .ok_or_else(|| SignerError::InvalidInput("invalid keypairs".to_string()))
115            })
116            .collect::<std::result::Result<_, SignerError>>()?;
117
118        Ok(Self {
119            signatures,
120            message,
121        })
122    }
123
124    pub fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
125        self.message.sanitize()?;
126        self.sanitize_signatures()?;
127        Ok(())
128    }
129
130    pub(crate) fn sanitize_signatures(&self) -> std::result::Result<(), SanitizeError> {
131        Self::sanitize_signatures_inner(
132            usize::from(self.message.header().num_required_signatures),
133            self.message.static_account_keys().len(),
134            self.signatures.len(),
135        )
136    }
137
138    pub(crate) fn sanitize_signatures_inner(
139        num_required_signatures: usize,
140        num_static_account_keys: usize,
141        num_signatures: usize,
142    ) -> std::result::Result<(), SanitizeError> {
143        match num_required_signatures.cmp(&num_signatures) {
144            Ordering::Greater => Err(SanitizeError::IndexOutOfBounds),
145            Ordering::Less => Err(SanitizeError::InvalidValue),
146            Ordering::Equal => Ok(()),
147        }?;
148
149        // Signatures are verified before message keys are loaded so all signers
150        // must correspond to static account keys.
151        if num_signatures > num_static_account_keys {
152            return Err(SanitizeError::IndexOutOfBounds);
153        }
154
155        Ok(())
156    }
157
158    /// Returns the version of the transaction
159    pub fn version(&self) -> TransactionVersion {
160        match self.message {
161            VersionedMessage::Legacy(_) => TransactionVersion::LEGACY,
162            VersionedMessage::V0(_) => TransactionVersion::Number(0),
163        }
164    }
165
166    /// Returns a legacy transaction if the transaction message is legacy.
167    pub fn into_legacy_transaction(self) -> Option<Transaction> {
168        match self.message {
169            VersionedMessage::Legacy(message) => Some(Transaction {
170                signatures: self.signatures,
171                message,
172            }),
173            _ => None,
174        }
175    }
176
177    #[cfg(feature = "verify")]
178    /// Verify the transaction and hash its message
179    pub fn verify_and_hash_message(
180        &self,
181    ) -> solana_transaction_error::TransactionResult<solana_hash::Hash> {
182        let message_bytes = self.message.serialize();
183        if !self
184            ._verify_with_results(&message_bytes)
185            .iter()
186            .all(|verify_result| *verify_result)
187        {
188            Err(solana_transaction_error::TransactionError::SignatureFailure)
189        } else {
190            Ok(VersionedMessage::hash_raw_message(&message_bytes))
191        }
192    }
193
194    #[cfg(feature = "verify")]
195    /// Verify the transaction and return a list of verification results
196    pub fn verify_with_results(&self) -> Vec<bool> {
197        let message_bytes = self.message.serialize();
198        self._verify_with_results(&message_bytes)
199    }
200
201    #[cfg(feature = "verify")]
202    fn _verify_with_results(&self, message_bytes: &[u8]) -> Vec<bool> {
203        self.signatures
204            .iter()
205            .zip(self.message.static_account_keys().iter())
206            .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), message_bytes))
207            .collect()
208    }
209
210    /// Returns true if transaction begins with an advance nonce instruction.
211    pub fn uses_durable_nonce(&self) -> bool {
212        let message = &self.message;
213        message
214            .instructions()
215            .get(crate::NONCED_TX_MARKER_IX_INDEX as usize)
216            .filter(|instruction| {
217                // Is system program
218                matches!(
219                    message.static_account_keys().get(instruction.program_id_index as usize),
220                    Some(program_id) if system_program::check_id(program_id)
221                ) && is_advance_nonce_instruction_data(&instruction.data)
222            })
223            .is_some()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use {
230        super::*,
231        solana_hash::Hash,
232        solana_instruction::{AccountMeta, Instruction},
233        solana_keypair::Keypair,
234        solana_message::Message as LegacyMessage,
235        solana_pubkey::Pubkey,
236        solana_signer::Signer,
237        solana_system_interface::instruction as system_instruction,
238    };
239
240    #[test]
241    fn test_try_new() {
242        let keypair0 = Keypair::new();
243        let keypair1 = Keypair::new();
244        let keypair2 = Keypair::new();
245
246        let message = VersionedMessage::Legacy(LegacyMessage::new(
247            &[Instruction::new_with_bytes(
248                Pubkey::new_unique(),
249                &[],
250                vec![
251                    AccountMeta::new_readonly(keypair1.pubkey(), true),
252                    AccountMeta::new_readonly(keypair2.pubkey(), false),
253                ],
254            )],
255            Some(&keypair0.pubkey()),
256        ));
257
258        assert_eq!(
259            VersionedTransaction::try_new(message.clone(), &[&keypair0]),
260            Err(SignerError::NotEnoughSigners)
261        );
262
263        assert_eq!(
264            VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair0]),
265            Err(SignerError::KeypairPubkeyMismatch)
266        );
267
268        assert_eq!(
269            VersionedTransaction::try_new(message.clone(), &[&keypair1, &keypair2]),
270            Err(SignerError::KeypairPubkeyMismatch)
271        );
272
273        match VersionedTransaction::try_new(message.clone(), &[&keypair0, &keypair1]) {
274            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
275            Err(err) => assert_eq!(Some(err), None),
276        }
277
278        match VersionedTransaction::try_new(message, &[&keypair1, &keypair0]) {
279            Ok(tx) => assert_eq!(tx.verify_with_results(), vec![true; 2]),
280            Err(err) => assert_eq!(Some(err), None),
281        }
282    }
283
284    fn nonced_transfer_tx() -> (Pubkey, Pubkey, VersionedTransaction) {
285        let from_keypair = Keypair::new();
286        let from_pubkey = from_keypair.pubkey();
287        let nonce_keypair = Keypair::new();
288        let nonce_pubkey = nonce_keypair.pubkey();
289        let instructions = [
290            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
291            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
292        ];
293        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
294        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
295        (from_pubkey, nonce_pubkey, tx.into())
296    }
297
298    #[test]
299    fn tx_uses_nonce_ok() {
300        let (_, _, tx) = nonced_transfer_tx();
301        assert!(tx.uses_durable_nonce());
302    }
303
304    #[test]
305    fn tx_uses_nonce_empty_ix_fail() {
306        assert!(!VersionedTransaction::default().uses_durable_nonce());
307    }
308
309    #[test]
310    fn tx_uses_nonce_bad_prog_id_idx_fail() {
311        let (_, _, mut tx) = nonced_transfer_tx();
312        match &mut tx.message {
313            VersionedMessage::Legacy(message) => {
314                message.instructions.get_mut(0).unwrap().program_id_index = 255u8;
315            }
316            VersionedMessage::V0(_) => unreachable!(),
317        };
318        assert!(!tx.uses_durable_nonce());
319    }
320
321    #[test]
322    fn tx_uses_nonce_first_prog_id_not_nonce_fail() {
323        let from_keypair = Keypair::new();
324        let from_pubkey = from_keypair.pubkey();
325        let nonce_keypair = Keypair::new();
326        let nonce_pubkey = nonce_keypair.pubkey();
327        let instructions = [
328            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
329            system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
330        ];
331        let message = LegacyMessage::new(&instructions, Some(&from_pubkey));
332        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
333        let tx = VersionedTransaction::from(tx);
334        assert!(!tx.uses_durable_nonce());
335    }
336
337    #[test]
338    fn tx_uses_nonce_wrong_first_nonce_ix_fail() {
339        let from_keypair = Keypair::new();
340        let from_pubkey = from_keypair.pubkey();
341        let nonce_keypair = Keypair::new();
342        let nonce_pubkey = nonce_keypair.pubkey();
343        let instructions = [
344            system_instruction::withdraw_nonce_account(
345                &nonce_pubkey,
346                &nonce_pubkey,
347                &from_pubkey,
348                42,
349            ),
350            system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42),
351        ];
352        let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey));
353        let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default());
354        let tx = VersionedTransaction::from(tx);
355        assert!(!tx.uses_durable_nonce());
356    }
357
358    #[test]
359    fn test_sanitize_signatures_inner() {
360        assert_eq!(
361            VersionedTransaction::sanitize_signatures_inner(1, 1, 0),
362            Err(SanitizeError::IndexOutOfBounds)
363        );
364        assert_eq!(
365            VersionedTransaction::sanitize_signatures_inner(1, 1, 2),
366            Err(SanitizeError::InvalidValue)
367        );
368        assert_eq!(
369            VersionedTransaction::sanitize_signatures_inner(2, 1, 2),
370            Err(SanitizeError::IndexOutOfBounds)
371        );
372        assert_eq!(
373            VersionedTransaction::sanitize_signatures_inner(1, 1, 1),
374            Ok(())
375        );
376    }
377
378    #[cfg(feature = "wincode")]
379    #[test]
380    fn versioned_transaction_wincode_bincode_roundtrip() {
381        use {
382            super::*,
383            proptest::prelude::*,
384            solana_address::{Address, ADDRESS_BYTES},
385            solana_hash::{Hash, HASH_BYTES},
386            solana_message::{
387                compiled_instruction::CompiledInstruction,
388                v0::{self, MessageAddressTableLookup},
389                Message as LegacyMessage, MessageHeader,
390            },
391            solana_signature::SIGNATURE_BYTES,
392        };
393
394        fn strat_byte_vec(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
395            proptest::collection::vec(any::<u8>(), 0..=max_len)
396        }
397
398        fn strat_signature() -> impl Strategy<Value = Signature> {
399            any::<[u8; SIGNATURE_BYTES]>().prop_map(Signature::from)
400        }
401
402        fn strat_address() -> impl Strategy<Value = Address> {
403            any::<[u8; ADDRESS_BYTES]>().prop_map(Address::new_from_array)
404        }
405
406        fn strat_hash() -> impl Strategy<Value = Hash> {
407            any::<[u8; HASH_BYTES]>().prop_map(Hash::new_from_array)
408        }
409
410        fn strat_message_header() -> impl Strategy<Value = MessageHeader> {
411            (0u8..128, any::<u8>(), any::<u8>()).prop_map(|(a, b, c)| MessageHeader {
412                num_required_signatures: a,
413                num_readonly_signed_accounts: b,
414                num_readonly_unsigned_accounts: c,
415            })
416        }
417
418        fn strat_compiled_instruction() -> impl Strategy<Value = CompiledInstruction> {
419            (any::<u8>(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
420                |(program_id_index, accounts, data)| {
421                    CompiledInstruction::new_from_raw_parts(program_id_index, accounts, data)
422                },
423            )
424        }
425
426        fn strat_address_table_lookup() -> impl Strategy<Value = MessageAddressTableLookup> {
427            (strat_address(), strat_byte_vec(128), strat_byte_vec(128)).prop_map(
428                |(account_key, writable_indexes, readonly_indexes)| MessageAddressTableLookup {
429                    account_key,
430                    writable_indexes,
431                    readonly_indexes,
432                },
433            )
434        }
435
436        fn strat_legacy_message() -> impl Strategy<Value = LegacyMessage> {
437            (
438                strat_message_header(),
439                proptest::collection::vec(strat_address(), 0..=8),
440                strat_hash(),
441                proptest::collection::vec(strat_compiled_instruction(), 0..=8),
442            )
443                .prop_map(|(header, account_keys, recent_blockhash, instructions)| {
444                    LegacyMessage {
445                        header,
446                        account_keys,
447                        recent_blockhash,
448                        instructions,
449                    }
450                })
451        }
452
453        fn strat_v0_message() -> impl Strategy<Value = v0::Message> {
454            (
455                strat_message_header(),
456                proptest::collection::vec(strat_address(), 0..=8),
457                strat_hash(),
458                proptest::collection::vec(strat_compiled_instruction(), 0..=4),
459                proptest::collection::vec(strat_address_table_lookup(), 0..=4),
460            )
461                .prop_map(
462                    |(
463                        header,
464                        account_keys,
465                        recent_blockhash,
466                        instructions,
467                        address_table_lookups,
468                    )| {
469                        v0::Message {
470                            header,
471                            account_keys,
472                            recent_blockhash,
473                            instructions,
474                            address_table_lookups,
475                        }
476                    },
477                )
478        }
479
480        fn strat_versioned_message() -> impl Strategy<Value = VersionedMessage> {
481            prop_oneof![
482                strat_legacy_message().prop_map(VersionedMessage::Legacy),
483                strat_v0_message().prop_map(VersionedMessage::V0),
484            ]
485        }
486
487        fn strat_versioned_transaction() -> impl Strategy<Value = VersionedTransaction> {
488            (
489                proptest::collection::vec(strat_signature(), 0..=8),
490                strat_versioned_message(),
491            )
492                .prop_map(|(signatures, message)| VersionedTransaction {
493                    signatures,
494                    message,
495                })
496        }
497
498        proptest!(|(tx in strat_versioned_transaction())| {
499            let bincode_serialized = bincode::serialize(&tx).unwrap();
500            let wincode_serialized = wincode::serialize(&tx).unwrap();
501            assert_eq!(bincode_serialized, wincode_serialized);
502
503            let bincode_deserialized: VersionedTransaction = bincode::deserialize(&bincode_serialized).unwrap();
504            let wincode_deserialized = wincode::deserialize(&wincode_serialized).unwrap();
505            assert_eq!(&bincode_deserialized, &wincode_deserialized);
506            assert_eq!(wincode_deserialized, tx);
507        });
508    }
509}