Skip to main content

solana_runtime_transaction/runtime_transaction/
sdk_transactions.rs

1use {
2    super::RuntimeTransaction,
3    crate::{
4        instruction_meta::InstructionMeta,
5        transaction_meta::{
6            CachedTransactionMeta, TransactionMeta, VersionedTransactionConfiguration,
7        },
8        transaction_with_meta::{StaticTransactionWithMeta, TransactionWithMeta},
9    },
10    solana_message::{AddressLoader, TransactionSignatureDetails},
11    solana_pubkey::Pubkey,
12    solana_svm_transaction::instruction::SVMInstruction,
13    solana_transaction::{
14        sanitized::{MessageHash, SanitizedTransaction},
15        simple_vote_transaction_checker::is_simple_vote_transaction,
16        versioned::{VersionedTransaction, sanitized::SanitizedVersionedTransaction},
17    },
18    solana_transaction_error::TransactionResult as Result,
19    std::{borrow::Cow, collections::HashSet},
20};
21
22impl RuntimeTransaction<SanitizedVersionedTransaction> {
23    pub fn try_from(
24        sanitized_versioned_tx: SanitizedVersionedTransaction,
25        message_hash: MessageHash,
26        is_simple_vote_tx: Option<bool>,
27    ) -> Result<Self> {
28        let message_hash = match message_hash {
29            MessageHash::Precomputed(hash) => hash,
30            MessageHash::Compute => sanitized_versioned_tx.get_message().message.hash(),
31        };
32        let is_simple_vote_tx = is_simple_vote_tx
33            .unwrap_or_else(|| is_simple_vote_transaction(&sanitized_versioned_tx));
34
35        let InstructionMeta {
36            precompile_signature_details,
37            instruction_data_len,
38        } = InstructionMeta::try_new(
39            sanitized_versioned_tx
40                .get_message()
41                .program_instructions_iter()
42                .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))),
43        )?;
44        let signature_details = TransactionSignatureDetails::new(
45            u64::from(
46                sanitized_versioned_tx
47                    .get_message()
48                    .message
49                    .header()
50                    .num_required_signatures,
51            ),
52            precompile_signature_details.num_secp256k1_instruction_signatures,
53            precompile_signature_details.num_ed25519_instruction_signatures,
54            precompile_signature_details.num_secp256r1_instruction_signatures,
55        );
56
57        let versioned_transaction_config =
58            VersionedTransactionConfiguration::try_from_sanitized_versioned_message(
59                sanitized_versioned_tx.get_message(),
60            )?;
61
62        Ok(Self {
63            transaction: sanitized_versioned_tx,
64            meta: CachedTransactionMeta {
65                message_hash,
66                is_simple_vote_transaction: is_simple_vote_tx,
67                signature_details,
68                versioned_transaction_config,
69                instruction_data_len,
70            },
71        })
72    }
73}
74
75impl RuntimeTransaction<SanitizedTransaction> {
76    /// Create a new `RuntimeTransaction<SanitizedTransaction>` from an
77    /// unsanitized `VersionedTransaction`.
78    pub fn try_create(
79        tx: VersionedTransaction,
80        message_hash: MessageHash,
81        is_simple_vote_tx: Option<bool>,
82        address_loader: impl AddressLoader,
83        reserved_account_keys: &HashSet<Pubkey>,
84    ) -> Result<Self> {
85        if tx.message.instructions().len()
86            > solana_transaction_context::MAX_INSTRUCTION_TRACE_LENGTH
87        {
88            return Err(solana_transaction_error::TransactionError::SanitizeFailure);
89        }
90
91        for instr in tx.message.instructions() {
92            if instr.accounts.len() > solana_transaction_context::MAX_ACCOUNTS_PER_INSTRUCTION {
93                return Err(solana_transaction_error::TransactionError::SanitizeFailure);
94            }
95        }
96
97        let statically_loaded_runtime_tx =
98            RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
99                SanitizedVersionedTransaction::try_from(tx)?,
100                message_hash,
101                is_simple_vote_tx,
102            )?;
103        Self::try_from(
104            statically_loaded_runtime_tx,
105            address_loader,
106            reserved_account_keys,
107        )
108    }
109
110    /// Create a new `RuntimeTransaction<SanitizedTransaction>` from a
111    /// `RuntimeTransaction<SanitizedVersionedTransaction>` that already has
112    /// static metadata loaded.
113    pub fn try_from(
114        statically_loaded_runtime_tx: RuntimeTransaction<SanitizedVersionedTransaction>,
115        address_loader: impl AddressLoader,
116        reserved_account_keys: &HashSet<Pubkey>,
117    ) -> Result<Self> {
118        let hash = *statically_loaded_runtime_tx.message_hash();
119        let is_simple_vote_tx = statically_loaded_runtime_tx.is_simple_vote_transaction();
120        let sanitized_transaction = SanitizedTransaction::try_new(
121            statically_loaded_runtime_tx.transaction,
122            hash,
123            is_simple_vote_tx,
124            address_loader,
125            reserved_account_keys,
126        )?;
127
128        let tx = Self {
129            transaction: sanitized_transaction,
130            meta: statically_loaded_runtime_tx.meta,
131        };
132
133        Ok(tx)
134    }
135}
136
137impl StaticTransactionWithMeta for RuntimeTransaction<SanitizedTransaction> {
138    #[inline]
139    fn to_versioned_transaction(&self) -> VersionedTransaction {
140        self.transaction.to_versioned_transaction()
141    }
142
143    fn serialized_size(&self) -> usize {
144        wincode::serialized_size(&self.to_versioned_transaction())
145            .expect("versioned transaction serialization should succeed") as usize
146    }
147}
148
149impl TransactionWithMeta for RuntimeTransaction<SanitizedTransaction> {
150    #[inline]
151    fn as_sanitized_transaction(&self) -> Cow<'_, SanitizedTransaction> {
152        Cow::Borrowed(self)
153    }
154}
155
156#[cfg(feature = "dev-context-only-utils")]
157impl RuntimeTransaction<SanitizedTransaction> {
158    pub fn from_transaction_for_tests(transaction: solana_transaction::Transaction) -> Self {
159        let versioned_transaction = VersionedTransaction::from(transaction);
160        Self::try_create(
161            versioned_transaction,
162            MessageHash::Compute,
163            None,
164            solana_message::SimpleAddressLoader::Disabled,
165            &HashSet::new(),
166        )
167        .expect("failed to create RuntimeTransaction from Transaction")
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use {
174        super::*,
175        agave_feature_set::FeatureSet,
176        agave_reserved_account_keys::ReservedAccountKeys,
177        solana_compute_budget_interface::ComputeBudgetInstruction,
178        solana_hash::Hash,
179        solana_instruction::{AccountMeta, Instruction},
180        solana_keypair::Keypair,
181        solana_message::{
182            Message, MessageHeader, SimpleAddressLoader, VersionedMessage,
183            compiled_instruction::CompiledInstruction,
184        },
185        solana_sdk_ids::vote,
186        solana_signature::Signature,
187        solana_signer::Signer,
188        solana_system_interface::instruction as system_instruction,
189        solana_transaction::{Transaction, versioned::VersionedTransaction},
190    };
191
192    fn vote_sanitized_versioned_transaction() -> SanitizedVersionedTransaction {
193        let block_hash = Hash::new_unique();
194        let vote_keypair = Keypair::new();
195        let node_keypair = Keypair::new();
196        let auth_keypair = Keypair::new();
197        let vote_ix = Instruction::new_with_bytes(
198            vote::id(),
199            &[],
200            vec![
201                AccountMeta::new(vote_keypair.pubkey(), false),
202                AccountMeta::new_readonly(auth_keypair.pubkey(), true),
203            ],
204        );
205        let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey()));
206        vote_tx.partial_sign(&[&node_keypair], block_hash);
207        vote_tx.partial_sign(&[&auth_keypair], block_hash);
208
209        SanitizedVersionedTransaction::try_from(VersionedTransaction::from(vote_tx)).unwrap()
210    }
211
212    fn non_vote_sanitized_versioned_transaction() -> SanitizedVersionedTransaction {
213        TestTransaction::new().to_sanitized_versioned_transaction()
214    }
215
216    // Simple transfer transaction for testing, it does not support vote instruction
217    // because simple vote transaction will not request limits
218    struct TestTransaction {
219        from_keypair: Keypair,
220        hash: Hash,
221        instructions: Vec<Instruction>,
222    }
223
224    impl TestTransaction {
225        fn new() -> Self {
226            let from_keypair = Keypair::new();
227            let instructions = vec![system_instruction::transfer(
228                &from_keypair.pubkey(),
229                &solana_pubkey::new_rand(),
230                1,
231            )];
232            TestTransaction {
233                from_keypair,
234                hash: Hash::new_unique(),
235                instructions,
236            }
237        }
238
239        fn add_compute_unit_limit(&mut self, val: u32) -> &mut TestTransaction {
240            self.instructions
241                .push(ComputeBudgetInstruction::set_compute_unit_limit(val));
242            self
243        }
244
245        fn add_compute_unit_price(&mut self, val: u64) -> &mut TestTransaction {
246            self.instructions
247                .push(ComputeBudgetInstruction::set_compute_unit_price(val));
248            self
249        }
250
251        fn add_loaded_accounts_bytes(&mut self, val: u32) -> &mut TestTransaction {
252            self.instructions
253                .push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(val));
254            self
255        }
256
257        fn to_sanitized_versioned_transaction(&self) -> SanitizedVersionedTransaction {
258            let message = Message::new(&self.instructions, Some(&self.from_keypair.pubkey()));
259            let tx = Transaction::new(&[&self.from_keypair], message, self.hash);
260            SanitizedVersionedTransaction::try_from(VersionedTransaction::from(tx)).unwrap()
261        }
262    }
263
264    #[test]
265    fn test_runtime_transaction_is_vote_meta() {
266        fn get_is_simple_vote(
267            svt: SanitizedVersionedTransaction,
268            is_simple_vote: Option<bool>,
269        ) -> bool {
270            RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
271                svt,
272                MessageHash::Compute,
273                is_simple_vote,
274            )
275            .unwrap()
276            .meta
277            .is_simple_vote_transaction
278        }
279
280        assert!(!get_is_simple_vote(
281            non_vote_sanitized_versioned_transaction(),
282            None
283        ));
284
285        assert!(get_is_simple_vote(
286            non_vote_sanitized_versioned_transaction(),
287            Some(true), // override
288        ));
289
290        assert!(get_is_simple_vote(
291            vote_sanitized_versioned_transaction(),
292            None
293        ));
294
295        assert!(!get_is_simple_vote(
296            vote_sanitized_versioned_transaction(),
297            Some(false), // override
298        ));
299    }
300
301    #[test]
302    fn test_advancing_transaction_type() {
303        let hash = Hash::new_unique();
304
305        let statically_loaded_transaction =
306            RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
307                non_vote_sanitized_versioned_transaction(),
308                MessageHash::Precomputed(hash),
309                None,
310            )
311            .unwrap();
312
313        assert_eq!(hash, *statically_loaded_transaction.message_hash());
314        assert!(!statically_loaded_transaction.is_simple_vote_transaction());
315
316        let dynamically_loaded_transaction = RuntimeTransaction::<SanitizedTransaction>::try_from(
317            statically_loaded_transaction,
318            SimpleAddressLoader::Disabled,
319            &ReservedAccountKeys::empty_key_set(),
320        );
321        let dynamically_loaded_transaction =
322            dynamically_loaded_transaction.expect("created from statically loaded tx");
323
324        assert_eq!(hash, *dynamically_loaded_transaction.message_hash());
325        assert!(!dynamically_loaded_transaction.is_simple_vote_transaction());
326    }
327
328    #[test]
329    fn test_runtime_transaction_static_meta() {
330        let hash = Hash::new_unique();
331        let compute_unit_limit = 250_000;
332        let compute_unit_price = 1_000;
333        let loaded_accounts_bytes = 1_024;
334        let mut test_transaction = TestTransaction::new();
335
336        let runtime_transaction_static =
337            RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
338                test_transaction
339                    .add_compute_unit_limit(compute_unit_limit)
340                    .add_compute_unit_price(compute_unit_price)
341                    .add_loaded_accounts_bytes(loaded_accounts_bytes)
342                    .to_sanitized_versioned_transaction(),
343                MessageHash::Precomputed(hash),
344                None,
345            )
346            .unwrap();
347
348        assert_eq!(&hash, runtime_transaction_static.message_hash());
349        assert!(!runtime_transaction_static.is_simple_vote_transaction());
350
351        let signature_details = &runtime_transaction_static.meta.signature_details;
352        assert_eq!(1, signature_details.num_transaction_signatures());
353        assert_eq!(0, signature_details.num_secp256k1_instruction_signatures());
354        assert_eq!(0, signature_details.num_ed25519_instruction_signatures());
355
356        for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] {
357            let transaction_configuration = runtime_transaction_static
358                .transaction_configuration(&feature_set)
359                .unwrap();
360            assert_eq!(
361                compute_unit_limit,
362                transaction_configuration.compute_unit_limit
363            );
364            assert_eq!(
365                compute_unit_price,
366                transaction_configuration.compute_unit_price_in_microlamports()
367            );
368            assert_eq!(
369                loaded_accounts_bytes,
370                transaction_configuration.loaded_accounts_data_size_limit
371            );
372        }
373    }
374
375    #[test]
376    fn test_serialized_size() {
377        let transaction = RuntimeTransaction::<SanitizedTransaction>::try_from(
378            RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
379                non_vote_sanitized_versioned_transaction(),
380                MessageHash::Compute,
381                None,
382            )
383            .unwrap(),
384            SimpleAddressLoader::Disabled,
385            &ReservedAccountKeys::empty_key_set(),
386        )
387        .unwrap();
388
389        let expected = wincode::serialized_size(&transaction.to_versioned_transaction()).unwrap();
390        assert_eq!(transaction.serialized_size(), expected as usize);
391        assert_eq!(
392            expected,
393            wincode::serialize(&transaction.to_versioned_transaction())
394                .unwrap()
395                .len() as u64
396        );
397    }
398
399    #[test]
400    fn test_simd_406_instruction_accounts_limit() {
401        let account_keys = vec![Pubkey::new_unique(); 3];
402        let header = MessageHeader {
403            num_required_signatures: 1,
404            num_readonly_signed_accounts: 0,
405            num_readonly_unsigned_accounts: 1,
406        };
407        let mut accounts: Vec<u8> = vec![0; 254];
408        // Exactly 255 accounts must pass sanitization
409        accounts.push(1);
410        let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts.clone());
411        let transaction = VersionedTransaction {
412            signatures: vec![Signature::default(); 1],
413            message: VersionedMessage::Legacy(solana_message::Message {
414                header,
415                account_keys: account_keys.clone(),
416                recent_blockhash: Hash::default(),
417                instructions: vec![instr],
418            }),
419        };
420        let result = RuntimeTransaction::<SanitizedTransaction>::try_create(
421            transaction,
422            MessageHash::Compute,
423            None,
424            solana_message::SimpleAddressLoader::Disabled,
425            &HashSet::new(),
426        );
427        assert!(result.is_ok());
428
429        // 256 accounts must fail
430        accounts.push(2);
431        let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts.clone());
432        let transaction = VersionedTransaction {
433            signatures: vec![Signature::default(); 1],
434            message: VersionedMessage::Legacy(solana_message::Message {
435                header,
436                account_keys,
437                recent_blockhash: Hash::default(),
438                instructions: vec![instr],
439            }),
440        };
441        let result = RuntimeTransaction::<SanitizedTransaction>::try_create(
442            transaction,
443            MessageHash::Compute,
444            None,
445            solana_message::SimpleAddressLoader::Disabled,
446            &HashSet::new(),
447        );
448        assert_eq!(
449            result.err(),
450            Some(solana_transaction_error::TransactionError::SanitizeFailure)
451        );
452    }
453}