solana_transaction/
sanitized.rs

1use {
2    crate::versioned::{sanitized::SanitizedVersionedTransaction, VersionedTransaction},
3    solana_hash::Hash,
4    solana_message::{
5        legacy,
6        v0::{self, LoadedAddresses},
7        AddressLoader, LegacyMessage, SanitizedMessage, SanitizedVersionedMessage,
8        VersionedMessage,
9    },
10    solana_pubkey::Pubkey,
11    solana_signature::Signature,
12    solana_transaction_error::{TransactionError, TransactionResult as Result},
13    std::collections::HashSet,
14};
15#[cfg(feature = "blake3")]
16use {crate::Transaction, solana_sanitize::Sanitize};
17
18/// Maximum number of accounts that a transaction may lock.
19/// 128 was chosen because it is the minimum number of accounts
20/// needed for the Neon EVM implementation.
21pub const MAX_TX_ACCOUNT_LOCKS: usize = 128;
22
23/// Sanitized transaction and the hash of its message
24#[derive(Debug, Clone, Eq, PartialEq)]
25pub struct SanitizedTransaction {
26    message: SanitizedMessage,
27    message_hash: Hash,
28    is_simple_vote_tx: bool,
29    signatures: Vec<Signature>,
30}
31
32/// Set of accounts that must be locked for safe transaction processing
33#[derive(Debug, Clone, Default, Eq, PartialEq)]
34pub struct TransactionAccountLocks<'a> {
35    /// List of readonly account key locks
36    pub readonly: Vec<&'a Pubkey>,
37    /// List of writable account key locks
38    pub writable: Vec<&'a Pubkey>,
39}
40
41/// Type that represents whether the transaction message has been precomputed or
42/// not.
43pub enum MessageHash {
44    Precomputed(Hash),
45    Compute,
46}
47
48impl From<Hash> for MessageHash {
49    fn from(hash: Hash) -> Self {
50        Self::Precomputed(hash)
51    }
52}
53
54impl SanitizedTransaction {
55    /// Create a sanitized transaction from a sanitized versioned transaction.
56    /// If the input transaction uses address tables, attempt to lookup the
57    /// address for each table index.
58    pub fn try_new(
59        tx: SanitizedVersionedTransaction,
60        message_hash: Hash,
61        is_simple_vote_tx: bool,
62        address_loader: impl AddressLoader,
63        reserved_account_keys: &HashSet<Pubkey>,
64    ) -> Result<Self> {
65        let signatures = tx.signatures;
66        let SanitizedVersionedMessage { message } = tx.message;
67        let message = match message {
68            VersionedMessage::Legacy(message) => {
69                SanitizedMessage::Legacy(LegacyMessage::new(message, reserved_account_keys))
70            }
71            VersionedMessage::V0(message) => {
72                let loaded_addresses =
73                    address_loader.load_addresses(&message.address_table_lookups)?;
74                SanitizedMessage::V0(v0::LoadedMessage::new(
75                    message,
76                    loaded_addresses,
77                    reserved_account_keys,
78                ))
79            }
80        };
81
82        Ok(Self {
83            message,
84            message_hash,
85            is_simple_vote_tx,
86            signatures,
87        })
88    }
89
90    #[cfg(feature = "blake3")]
91    /// Create a sanitized transaction from an un-sanitized versioned
92    /// transaction.  If the input transaction uses address tables, attempt to
93    /// lookup the address for each table index.
94    pub fn try_create(
95        tx: VersionedTransaction,
96        message_hash: impl Into<MessageHash>,
97        is_simple_vote_tx: Option<bool>,
98        address_loader: impl AddressLoader,
99        reserved_account_keys: &HashSet<Pubkey>,
100    ) -> Result<Self> {
101        let sanitized_versioned_tx = SanitizedVersionedTransaction::try_from(tx)?;
102        let is_simple_vote_tx = is_simple_vote_tx.unwrap_or_else(|| {
103            crate::simple_vote_transaction_checker::is_simple_vote_transaction(
104                &sanitized_versioned_tx,
105            )
106        });
107        let message_hash = match message_hash.into() {
108            MessageHash::Compute => sanitized_versioned_tx.message.message.hash(),
109            MessageHash::Precomputed(hash) => hash,
110        };
111        Self::try_new(
112            sanitized_versioned_tx,
113            message_hash,
114            is_simple_vote_tx,
115            address_loader,
116            reserved_account_keys,
117        )
118    }
119
120    /// Create a sanitized transaction from a legacy transaction
121    #[cfg(feature = "blake3")]
122    pub fn try_from_legacy_transaction(
123        tx: Transaction,
124        reserved_account_keys: &HashSet<Pubkey>,
125    ) -> Result<Self> {
126        tx.sanitize()?;
127
128        Ok(Self {
129            message_hash: tx.message.hash(),
130            message: SanitizedMessage::Legacy(LegacyMessage::new(
131                tx.message,
132                reserved_account_keys,
133            )),
134            is_simple_vote_tx: false,
135            signatures: tx.signatures,
136        })
137    }
138
139    /// Create a sanitized transaction from a legacy transaction. Used for tests only.
140    #[cfg(feature = "blake3")]
141    pub fn from_transaction_for_tests(tx: Transaction) -> Self {
142        let empty_key_set = HashSet::default();
143        Self::try_from_legacy_transaction(tx, &empty_key_set).unwrap()
144    }
145
146    /// Create a sanitized transaction from fields.
147    /// Performs only basic signature sanitization.
148    pub fn try_new_from_fields(
149        message: SanitizedMessage,
150        message_hash: Hash,
151        is_simple_vote_tx: bool,
152        signatures: Vec<Signature>,
153    ) -> Result<Self> {
154        VersionedTransaction::sanitize_signatures_inner(
155            usize::from(message.header().num_required_signatures),
156            message.static_account_keys().len(),
157            signatures.len(),
158        )?;
159
160        Ok(Self {
161            message,
162            message_hash,
163            signatures,
164            is_simple_vote_tx,
165        })
166    }
167
168    /// Return the first signature for this transaction.
169    ///
170    /// Notes:
171    ///
172    /// Sanitized transactions must have at least one signature because the
173    /// number of signatures must be greater than or equal to the message header
174    /// value `num_required_signatures` which must be greater than 0 itself.
175    pub fn signature(&self) -> &Signature {
176        &self.signatures[0]
177    }
178
179    /// Return the list of signatures for this transaction
180    pub fn signatures(&self) -> &[Signature] {
181        &self.signatures
182    }
183
184    /// Return the signed message
185    pub fn message(&self) -> &SanitizedMessage {
186        &self.message
187    }
188
189    /// Return the hash of the signed message
190    pub fn message_hash(&self) -> &Hash {
191        &self.message_hash
192    }
193
194    /// Returns true if this transaction is a simple vote
195    pub fn is_simple_vote_transaction(&self) -> bool {
196        self.is_simple_vote_tx
197    }
198
199    /// Convert this sanitized transaction into a versioned transaction for
200    /// recording in the ledger.
201    pub fn to_versioned_transaction(&self) -> VersionedTransaction {
202        let signatures = self.signatures.clone();
203        match &self.message {
204            SanitizedMessage::V0(sanitized_msg) => VersionedTransaction {
205                signatures,
206                message: VersionedMessage::V0(v0::Message::clone(&sanitized_msg.message)),
207            },
208            SanitizedMessage::Legacy(legacy_message) => VersionedTransaction {
209                signatures,
210                message: VersionedMessage::Legacy(legacy::Message::clone(&legacy_message.message)),
211            },
212        }
213    }
214
215    /// Validate and return the account keys locked by this transaction
216    pub fn get_account_locks(
217        &self,
218        tx_account_lock_limit: usize,
219    ) -> Result<TransactionAccountLocks<'_>> {
220        Self::validate_account_locks(self.message(), tx_account_lock_limit)?;
221        Ok(self.get_account_locks_unchecked())
222    }
223
224    /// Return the list of accounts that must be locked during processing this transaction.
225    pub fn get_account_locks_unchecked(&self) -> TransactionAccountLocks<'_> {
226        let message = &self.message;
227        let account_keys = message.account_keys();
228        let num_readonly_accounts = message.num_readonly_accounts();
229        let num_writable_accounts = account_keys.len().saturating_sub(num_readonly_accounts);
230
231        let mut account_locks = TransactionAccountLocks {
232            writable: Vec::with_capacity(num_writable_accounts),
233            readonly: Vec::with_capacity(num_readonly_accounts),
234        };
235
236        for (i, key) in account_keys.iter().enumerate() {
237            if message.is_writable(i) {
238                account_locks.writable.push(key);
239            } else {
240                account_locks.readonly.push(key);
241            }
242        }
243
244        account_locks
245    }
246
247    /// Return the list of addresses loaded from on-chain address lookup tables
248    pub fn get_loaded_addresses(&self) -> LoadedAddresses {
249        match &self.message {
250            SanitizedMessage::Legacy(_) => LoadedAddresses::default(),
251            SanitizedMessage::V0(message) => LoadedAddresses::clone(&message.loaded_addresses),
252        }
253    }
254
255    /// If the transaction uses a durable nonce, return the pubkey of the nonce account
256    #[cfg(feature = "bincode")]
257    pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
258        self.message.get_durable_nonce()
259    }
260
261    #[cfg(feature = "verify")]
262    /// Return the serialized message data to sign.
263    fn message_data(&self) -> Vec<u8> {
264        match &self.message {
265            SanitizedMessage::Legacy(legacy_message) => legacy_message.message.serialize(),
266            SanitizedMessage::V0(loaded_msg) => loaded_msg.message.serialize(),
267        }
268    }
269
270    #[cfg(feature = "verify")]
271    /// Verify the transaction signatures
272    pub fn verify(&self) -> Result<()> {
273        let message_bytes = self.message_data();
274        if self
275            .signatures
276            .iter()
277            .zip(self.message.account_keys().iter())
278            .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
279            .any(|verified| !verified)
280        {
281            Err(TransactionError::SignatureFailure)
282        } else {
283            Ok(())
284        }
285    }
286
287    /// Validate a transaction message against locked accounts
288    pub fn validate_account_locks(
289        message: &SanitizedMessage,
290        tx_account_lock_limit: usize,
291    ) -> Result<()> {
292        if message.has_duplicates() {
293            Err(TransactionError::AccountLoadedTwice)
294        } else if message.account_keys().len() > tx_account_lock_limit {
295            Err(TransactionError::TooManyAccountLocks)
296        } else {
297            Ok(())
298        }
299    }
300
301    #[cfg(feature = "dev-context-only-utils")]
302    pub fn new_for_tests(
303        message: SanitizedMessage,
304        signatures: Vec<Signature>,
305        is_simple_vote_tx: bool,
306    ) -> SanitizedTransaction {
307        SanitizedTransaction {
308            message,
309            message_hash: Hash::new_unique(),
310            signatures,
311            is_simple_vote_tx,
312        }
313    }
314}
315
316#[cfg(test)]
317#[allow(clippy::arithmetic_side_effects)]
318mod tests {
319    use {
320        super::*,
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                    Pubkey::new_unique(),
407                    Pubkey::new_unique(),
408                    Pubkey::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}