Skip to main content

solana_transaction/
sanitized.rs

1use {
2    crate::versioned::{sanitized::SanitizedVersionedTransaction, VersionedTransaction},
3    alloc::vec::Vec,
4    solana_address::Address,
5    solana_hash::Hash,
6    solana_message::{
7        legacy,
8        v0::{self, LoadedAddresses},
9        v1::{self, CachedMessage},
10        AddressLoader, LegacyMessage, SanitizedMessage, SanitizedVersionedMessage,
11        VersionedMessage,
12    },
13    solana_signature::Signature,
14    solana_transaction_error::{TransactionError, TransactionResult},
15    std::collections::HashSet,
16};
17#[cfg(feature = "blake3")]
18use {crate::Transaction, solana_sanitize::Sanitize};
19
20/// Maximum number of accounts that a transaction may lock.
21/// 128 was chosen because it is the minimum number of accounts
22/// needed for the Neon EVM implementation.
23pub const MAX_TX_ACCOUNT_LOCKS: usize = 128;
24
25/// Sanitized transaction and the hash of its message
26#[derive(Debug, Clone, Eq, PartialEq)]
27pub struct SanitizedTransaction {
28    message: SanitizedMessage,
29    message_hash: Hash,
30    is_simple_vote_tx: bool,
31    signatures: Vec<Signature>,
32}
33
34/// Set of accounts that must be locked for safe transaction processing
35#[derive(Debug, Clone, Default, Eq, PartialEq)]
36pub struct TransactionAccountLocks<'a> {
37    /// List of readonly account key locks
38    pub readonly: Vec<&'a Address>,
39    /// List of writable account key locks
40    pub writable: Vec<&'a Address>,
41}
42
43/// Type that represents whether the transaction message has been precomputed or
44/// not.
45pub enum MessageHash {
46    Precomputed(Hash),
47    Compute,
48}
49
50impl From<Hash> for MessageHash {
51    fn from(hash: Hash) -> Self {
52        Self::Precomputed(hash)
53    }
54}
55
56impl SanitizedTransaction {
57    /// Create a sanitized transaction from a sanitized versioned transaction.
58    /// If the input transaction uses address tables, attempt to lookup the
59    /// address for each table index.
60    pub fn try_new(
61        tx: SanitizedVersionedTransaction,
62        message_hash: Hash,
63        is_simple_vote_tx: bool,
64        address_loader: impl AddressLoader,
65        reserved_account_keys: &HashSet<Address>,
66    ) -> TransactionResult<Self> {
67        let signatures = tx.signatures;
68        let SanitizedVersionedMessage { message } = tx.message;
69        let message = match message {
70            VersionedMessage::Legacy(message) => {
71                SanitizedMessage::Legacy(LegacyMessage::new(message, reserved_account_keys))
72            }
73            VersionedMessage::V0(message) => {
74                let loaded_addresses =
75                    address_loader.load_addresses(&message.address_table_lookups)?;
76                SanitizedMessage::V0(v0::LoadedMessage::new(
77                    message,
78                    loaded_addresses,
79                    reserved_account_keys,
80                ))
81            }
82            VersionedMessage::V1(message) => {
83                SanitizedMessage::V1(CachedMessage::new(message, reserved_account_keys))
84            }
85        };
86
87        Ok(Self {
88            message,
89            message_hash,
90            is_simple_vote_tx,
91            signatures,
92        })
93    }
94
95    #[cfg(feature = "blake3")]
96    /// Create a sanitized transaction from an un-sanitized versioned
97    /// transaction.  If the input transaction uses address tables, attempt to
98    /// lookup the address for each table index.
99    pub fn try_create(
100        tx: VersionedTransaction,
101        message_hash: impl Into<MessageHash>,
102        is_simple_vote_tx: Option<bool>,
103        address_loader: impl AddressLoader,
104        reserved_account_keys: &HashSet<Address>,
105    ) -> TransactionResult<Self> {
106        let sanitized_versioned_tx = SanitizedVersionedTransaction::try_from(tx)?;
107        let is_simple_vote_tx = is_simple_vote_tx.unwrap_or_else(|| {
108            crate::simple_vote_transaction_checker::is_simple_vote_transaction(
109                &sanitized_versioned_tx,
110            )
111        });
112        let message_hash = match message_hash.into() {
113            MessageHash::Compute => sanitized_versioned_tx.message.message.hash(),
114            MessageHash::Precomputed(hash) => hash,
115        };
116        Self::try_new(
117            sanitized_versioned_tx,
118            message_hash,
119            is_simple_vote_tx,
120            address_loader,
121            reserved_account_keys,
122        )
123    }
124
125    /// Create a sanitized transaction from a legacy transaction
126    #[cfg(feature = "blake3")]
127    pub fn try_from_legacy_transaction(
128        tx: Transaction,
129        reserved_account_keys: &HashSet<Address>,
130    ) -> TransactionResult<Self> {
131        tx.sanitize()?;
132
133        Ok(Self {
134            message_hash: tx.message.hash(),
135            message: SanitizedMessage::Legacy(LegacyMessage::new(
136                tx.message,
137                reserved_account_keys,
138            )),
139            is_simple_vote_tx: false,
140            signatures: tx.signatures,
141        })
142    }
143
144    /// Create a sanitized transaction from a legacy transaction. Used for tests only.
145    #[cfg(feature = "blake3")]
146    pub fn from_transaction_for_tests(tx: Transaction) -> Self {
147        let empty_key_set = HashSet::default();
148        Self::try_from_legacy_transaction(tx, &empty_key_set).unwrap()
149    }
150
151    /// Create a sanitized transaction from fields.
152    /// Performs only basic signature sanitization.
153    pub fn try_new_from_fields(
154        message: SanitizedMessage,
155        message_hash: Hash,
156        is_simple_vote_tx: bool,
157        signatures: Vec<Signature>,
158    ) -> TransactionResult<Self> {
159        VersionedTransaction::sanitize_signatures_inner(
160            usize::from(message.header().num_required_signatures),
161            message.static_account_keys().len(),
162            signatures.len(),
163        )?;
164
165        Ok(Self {
166            message,
167            message_hash,
168            signatures,
169            is_simple_vote_tx,
170        })
171    }
172
173    /// Return the first signature for this transaction.
174    ///
175    /// Notes:
176    ///
177    /// Sanitized transactions must have at least one signature because the
178    /// number of signatures must be greater than or equal to the message header
179    /// value `num_required_signatures` which must be greater than 0 itself.
180    pub fn signature(&self) -> &Signature {
181        &self.signatures[0]
182    }
183
184    /// Return the list of signatures for this transaction
185    pub fn signatures(&self) -> &[Signature] {
186        &self.signatures
187    }
188
189    /// Return the signed message
190    pub fn message(&self) -> &SanitizedMessage {
191        &self.message
192    }
193
194    /// Return the hash of the signed message
195    pub fn message_hash(&self) -> &Hash {
196        &self.message_hash
197    }
198
199    /// Returns true if this transaction is a simple vote
200    pub fn is_simple_vote_transaction(&self) -> bool {
201        self.is_simple_vote_tx
202    }
203
204    /// Convert this sanitized transaction into a versioned transaction for
205    /// recording in the ledger.
206    pub fn to_versioned_transaction(&self) -> VersionedTransaction {
207        let signatures = self.signatures.clone();
208        match &self.message {
209            SanitizedMessage::Legacy(legacy_message) => VersionedTransaction {
210                message: VersionedMessage::Legacy(legacy::Message::clone(&legacy_message.message)),
211                signatures,
212            },
213            SanitizedMessage::V0(sanitized_msg) => VersionedTransaction {
214                signatures,
215                message: VersionedMessage::V0(v0::Message::clone(&sanitized_msg.message)),
216            },
217            SanitizedMessage::V1(sanitized_msg) => VersionedTransaction {
218                message: VersionedMessage::V1(v1::Message::clone(&sanitized_msg.message)),
219                signatures,
220            },
221        }
222    }
223
224    /// Validate and return the account keys locked by this transaction
225    pub fn get_account_locks(
226        &self,
227        tx_account_lock_limit: usize,
228    ) -> TransactionResult<TransactionAccountLocks<'_>> {
229        Self::validate_account_locks(self.message(), tx_account_lock_limit)?;
230        Ok(self.get_account_locks_unchecked())
231    }
232
233    /// Return the list of accounts that must be locked during processing this transaction.
234    pub fn get_account_locks_unchecked(&self) -> TransactionAccountLocks<'_> {
235        let message = &self.message;
236        let account_keys = message.account_keys();
237        let num_readonly_accounts = message.num_readonly_accounts();
238        let num_writable_accounts = account_keys.len().saturating_sub(num_readonly_accounts);
239
240        let mut account_locks = TransactionAccountLocks {
241            writable: Vec::with_capacity(num_writable_accounts),
242            readonly: Vec::with_capacity(num_readonly_accounts),
243        };
244
245        for (i, key) in account_keys.iter().enumerate() {
246            if message.is_writable(i) {
247                account_locks.writable.push(key);
248            } else {
249                account_locks.readonly.push(key);
250            }
251        }
252
253        account_locks
254    }
255
256    /// Return the list of addresses loaded from on-chain address lookup tables
257    pub fn get_loaded_addresses(&self) -> LoadedAddresses {
258        match &self.message {
259            SanitizedMessage::Legacy(_) | SanitizedMessage::V1(_) => LoadedAddresses::default(),
260            SanitizedMessage::V0(message) => LoadedAddresses::clone(&message.loaded_addresses),
261        }
262    }
263
264    #[cfg(feature = "verify")]
265    /// Return the serialized message data to sign.
266    fn message_data(&self) -> Vec<u8> {
267        match &self.message {
268            SanitizedMessage::Legacy(legacy_message) => legacy_message.message.serialize(),
269            SanitizedMessage::V0(loaded_msg) => loaded_msg.message.serialize(),
270            SanitizedMessage::V1(cached_msg) => cached_msg.message.serialize(),
271        }
272    }
273
274    #[cfg(feature = "verify")]
275    /// Verify the transaction signatures
276    pub fn verify(&self) -> TransactionResult<()> {
277        let message_bytes = self.message_data();
278        crate::verify_signatures(
279            &self.signatures,
280            self.message.static_account_keys(),
281            &message_bytes,
282        )
283    }
284
285    /// Validate a transaction message against locked accounts
286    pub fn validate_account_locks(
287        message: &SanitizedMessage,
288        tx_account_lock_limit: usize,
289    ) -> TransactionResult<()> {
290        if message.has_duplicates() {
291            Err(TransactionError::AccountLoadedTwice)
292        } else if message.account_keys().len() > tx_account_lock_limit {
293            Err(TransactionError::TooManyAccountLocks)
294        } else {
295            Ok(())
296        }
297    }
298
299    #[cfg(feature = "dev-context-only-utils")]
300    pub fn new_for_tests(
301        message: SanitizedMessage,
302        signatures: Vec<Signature>,
303        is_simple_vote_tx: bool,
304    ) -> SanitizedTransaction {
305        SanitizedTransaction {
306            message,
307            message_hash: Hash::new_unique(),
308            signatures,
309            is_simple_vote_tx,
310        }
311    }
312}
313
314#[cfg(test)]
315#[allow(clippy::arithmetic_side_effects)]
316mod tests {
317    use {
318        super::*,
319        alloc::vec,
320        solana_instruction::{AccountMeta, Instruction},
321        solana_keypair::Keypair,
322        solana_message::{MessageHeader, SimpleAddressLoader},
323        solana_signer::Signer,
324        solana_vote_interface::{instruction, state::Vote},
325    };
326
327    #[test]
328    fn test_try_create_simple_vote_tx() {
329        let bank_hash = Hash::default();
330        let block_hash = Hash::default();
331        let empty_key_set = HashSet::default();
332        let vote_keypair = Keypair::new();
333        let node_keypair = Keypair::new();
334        let auth_keypair = Keypair::new();
335        let votes = Vote::new(vec![1, 2, 3], bank_hash);
336        let vote_ix = instruction::vote(&vote_keypair.pubkey(), &auth_keypair.pubkey(), votes);
337        let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey()));
338        vote_tx.partial_sign(&[&node_keypair], block_hash);
339        vote_tx.partial_sign(&[&auth_keypair], block_hash);
340
341        // single legacy vote ix, 2 signatures
342        {
343            let vote_transaction = SanitizedTransaction::try_create(
344                VersionedTransaction::from(vote_tx.clone()),
345                MessageHash::Compute,
346                None,
347                SimpleAddressLoader::Disabled,
348                &empty_key_set,
349            )
350            .unwrap();
351            assert!(vote_transaction.is_simple_vote_transaction());
352        }
353
354        {
355            // call side says it is not a vote
356            let vote_transaction = SanitizedTransaction::try_create(
357                VersionedTransaction::from(vote_tx.clone()),
358                MessageHash::Compute,
359                Some(false),
360                SimpleAddressLoader::Disabled,
361                &empty_key_set,
362            )
363            .unwrap();
364            assert!(!vote_transaction.is_simple_vote_transaction());
365        }
366
367        // single legacy vote ix, 3 signatures
368        vote_tx.signatures.push(Signature::default());
369        vote_tx.message.header.num_required_signatures = 3;
370        {
371            let vote_transaction = SanitizedTransaction::try_create(
372                VersionedTransaction::from(vote_tx.clone()),
373                MessageHash::Compute,
374                None,
375                SimpleAddressLoader::Disabled,
376                &empty_key_set,
377            )
378            .unwrap();
379            assert!(!vote_transaction.is_simple_vote_transaction());
380        }
381
382        {
383            // call site says it is simple vote
384            let vote_transaction = SanitizedTransaction::try_create(
385                VersionedTransaction::from(vote_tx),
386                MessageHash::Compute,
387                Some(true),
388                SimpleAddressLoader::Disabled,
389                &empty_key_set,
390            )
391            .unwrap();
392            assert!(vote_transaction.is_simple_vote_transaction());
393        }
394    }
395
396    #[test]
397    fn test_try_new_from_fields() {
398        let legacy_message = SanitizedMessage::try_from_legacy_message(
399            legacy::Message {
400                header: MessageHeader {
401                    num_required_signatures: 2,
402                    num_readonly_signed_accounts: 1,
403                    num_readonly_unsigned_accounts: 1,
404                },
405                account_keys: vec![
406                    Address::new_unique(),
407                    Address::new_unique(),
408                    Address::new_unique(),
409                ],
410                ..legacy::Message::default()
411            },
412            &HashSet::default(),
413        )
414        .unwrap();
415
416        for is_simple_vote_tx in [false, true] {
417            // Not enough signatures
418            assert!(SanitizedTransaction::try_new_from_fields(
419                legacy_message.clone(),
420                Hash::new_unique(),
421                is_simple_vote_tx,
422                vec![],
423            )
424            .is_err());
425            // Too many signatures
426            assert!(SanitizedTransaction::try_new_from_fields(
427                legacy_message.clone(),
428                Hash::new_unique(),
429                is_simple_vote_tx,
430                vec![
431                    Signature::default(),
432                    Signature::default(),
433                    Signature::default()
434                ],
435            )
436            .is_err());
437            // Correct number of signatures.
438            assert!(SanitizedTransaction::try_new_from_fields(
439                legacy_message.clone(),
440                Hash::new_unique(),
441                is_simple_vote_tx,
442                vec![Signature::default(), Signature::default()]
443            )
444            .is_ok());
445        }
446    }
447
448    #[test]
449    fn test_verify_v1_message_data_includes_prefix() {
450        let payer = Keypair::new();
451        let program_id = Address::new_unique();
452        let instruction = Instruction::new_with_bytes(
453            program_id,
454            &[1, 2, 3],
455            vec![AccountMeta::new(payer.pubkey(), true)],
456        );
457        let message = v1::Message::try_compile_with_config(
458            &payer.pubkey(),
459            &[instruction],
460            Hash::new_unique(),
461            v1::TransactionConfig::empty(),
462        )
463        .unwrap();
464        let versioned_tx =
465            VersionedTransaction::try_new(VersionedMessage::V1(message), &[&payer]).unwrap();
466
467        let sanitized = SanitizedTransaction::try_create(
468            versioned_tx,
469            MessageHash::Compute,
470            None,
471            SimpleAddressLoader::Disabled,
472            &HashSet::default(),
473        )
474        .unwrap();
475
476        // The signed bytes must begin with the V1 version prefix.
477        let signed_bytes = sanitized.message_data();
478        assert_eq!(signed_bytes[0], solana_message::v1::V1_PREFIX);
479
480        sanitized.verify().unwrap();
481    }
482}