solana_sdk/transaction/
sanitized.rs

1#![cfg(feature = "full")]
2
3pub use crate::message::{AddressLoader, SimpleAddressLoader};
4use {
5    super::SanitizedVersionedTransaction,
6    crate::{
7        hash::Hash,
8        message::{
9            legacy,
10            v0::{self, LoadedAddresses},
11            LegacyMessage, SanitizedMessage, VersionedMessage,
12        },
13        precompiles::verify_if_precompile,
14        pubkey::Pubkey,
15        reserved_account_keys::ReservedAccountKeys,
16        signature::Signature,
17        simple_vote_transaction_checker::is_simple_vote_transaction,
18        transaction::{Result, Transaction, VersionedTransaction},
19    },
20    solana_feature_set as feature_set,
21    solana_program::{instruction::InstructionError, message::SanitizedVersionedMessage},
22    solana_sanitize::Sanitize,
23    solana_transaction_error::TransactionError,
24    std::collections::HashSet,
25};
26
27/// Maximum number of accounts that a transaction may lock.
28/// 128 was chosen because it is the minimum number of accounts
29/// needed for the Neon EVM implementation.
30pub const MAX_TX_ACCOUNT_LOCKS: usize = 128;
31
32/// Sanitized transaction and the hash of its message
33#[derive(Debug, Clone, Eq, PartialEq)]
34pub struct SanitizedTransaction {
35    message: SanitizedMessage,
36    message_hash: Hash,
37    is_simple_vote_tx: bool,
38    signatures: Vec<Signature>,
39}
40
41/// Set of accounts that must be locked for safe transaction processing
42#[derive(Debug, Clone, Default, Eq, PartialEq)]
43pub struct TransactionAccountLocks<'a> {
44    /// List of readonly account key locks
45    pub readonly: Vec<&'a Pubkey>,
46    /// List of writable account key locks
47    pub writable: Vec<&'a Pubkey>,
48}
49
50/// Type that represents whether the transaction message has been precomputed or
51/// not.
52pub enum MessageHash {
53    Precomputed(Hash),
54    Compute,
55}
56
57impl From<Hash> for MessageHash {
58    fn from(hash: Hash) -> Self {
59        Self::Precomputed(hash)
60    }
61}
62
63impl SanitizedTransaction {
64    /// Create a sanitized transaction from a sanitized versioned transaction.
65    /// If the input transaction uses address tables, attempt to lookup the
66    /// address for each table index.
67    pub fn try_new(
68        tx: SanitizedVersionedTransaction,
69        message_hash: Hash,
70        is_simple_vote_tx: bool,
71        address_loader: impl AddressLoader,
72        reserved_account_keys: &HashSet<Pubkey>,
73    ) -> Result<Self> {
74        let signatures = tx.signatures;
75        let SanitizedVersionedMessage { message } = tx.message;
76        let message = match message {
77            VersionedMessage::Legacy(message) => {
78                SanitizedMessage::Legacy(LegacyMessage::new(message, reserved_account_keys))
79            }
80            VersionedMessage::V0(message) => {
81                let loaded_addresses =
82                    address_loader.load_addresses(&message.address_table_lookups)?;
83                SanitizedMessage::V0(v0::LoadedMessage::new(
84                    message,
85                    loaded_addresses,
86                    reserved_account_keys,
87                ))
88            }
89        };
90
91        Ok(Self {
92            message,
93            message_hash,
94            is_simple_vote_tx,
95            signatures,
96        })
97    }
98
99    /// Create a sanitized transaction from an un-sanitized versioned
100    /// transaction.  If the input transaction uses address tables, attempt to
101    /// lookup the address for each table index.
102    pub fn try_create(
103        tx: VersionedTransaction,
104        message_hash: impl Into<MessageHash>,
105        is_simple_vote_tx: Option<bool>,
106        address_loader: impl AddressLoader,
107        reserved_account_keys: &HashSet<Pubkey>,
108    ) -> Result<Self> {
109        let sanitized_versioned_tx = SanitizedVersionedTransaction::try_from(tx)?;
110        let is_simple_vote_tx = is_simple_vote_tx
111            .unwrap_or_else(|| is_simple_vote_transaction(&sanitized_versioned_tx));
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    pub fn try_from_legacy_transaction(
127        tx: Transaction,
128        reserved_account_keys: &HashSet<Pubkey>,
129    ) -> Result<Self> {
130        tx.sanitize()?;
131
132        Ok(Self {
133            message_hash: tx.message.hash(),
134            message: SanitizedMessage::Legacy(LegacyMessage::new(
135                tx.message,
136                reserved_account_keys,
137            )),
138            is_simple_vote_tx: false,
139            signatures: tx.signatures,
140        })
141    }
142
143    /// Create a sanitized transaction from a legacy transaction. Used for tests only.
144    pub fn from_transaction_for_tests(tx: Transaction) -> Self {
145        Self::try_from_legacy_transaction(tx, &ReservedAccountKeys::empty_key_set()).unwrap()
146    }
147
148    /// Return the first signature for this transaction.
149    ///
150    /// Notes:
151    ///
152    /// Sanitized transactions must have at least one signature because the
153    /// number of signatures must be greater than or equal to the message header
154    /// value `num_required_signatures` which must be greater than 0 itself.
155    pub fn signature(&self) -> &Signature {
156        &self.signatures[0]
157    }
158
159    /// Return the list of signatures for this transaction
160    pub fn signatures(&self) -> &[Signature] {
161        &self.signatures
162    }
163
164    /// Return the signed message
165    pub fn message(&self) -> &SanitizedMessage {
166        &self.message
167    }
168
169    /// Return the hash of the signed message
170    pub fn message_hash(&self) -> &Hash {
171        &self.message_hash
172    }
173
174    /// Returns true if this transaction is a simple vote
175    pub fn is_simple_vote_transaction(&self) -> bool {
176        self.is_simple_vote_tx
177    }
178
179    /// Convert this sanitized transaction into a versioned transaction for
180    /// recording in the ledger.
181    pub fn to_versioned_transaction(&self) -> VersionedTransaction {
182        let signatures = self.signatures.clone();
183        match &self.message {
184            SanitizedMessage::V0(sanitized_msg) => VersionedTransaction {
185                signatures,
186                message: VersionedMessage::V0(v0::Message::clone(&sanitized_msg.message)),
187            },
188            SanitizedMessage::Legacy(legacy_message) => VersionedTransaction {
189                signatures,
190                message: VersionedMessage::Legacy(legacy::Message::clone(&legacy_message.message)),
191            },
192        }
193    }
194
195    /// Validate and return the account keys locked by this transaction
196    pub fn get_account_locks(
197        &self,
198        tx_account_lock_limit: usize,
199    ) -> Result<TransactionAccountLocks> {
200        Self::validate_account_locks(self.message(), tx_account_lock_limit)?;
201        Ok(self.get_account_locks_unchecked())
202    }
203
204    /// Return the list of accounts that must be locked during processing this transaction.
205    pub fn get_account_locks_unchecked(&self) -> TransactionAccountLocks {
206        let message = &self.message;
207        let account_keys = message.account_keys();
208        let num_readonly_accounts = message.num_readonly_accounts();
209        let num_writable_accounts = account_keys.len().saturating_sub(num_readonly_accounts);
210
211        let mut account_locks = TransactionAccountLocks {
212            writable: Vec::with_capacity(num_writable_accounts),
213            readonly: Vec::with_capacity(num_readonly_accounts),
214        };
215
216        for (i, key) in account_keys.iter().enumerate() {
217            if message.is_writable(i) {
218                account_locks.writable.push(key);
219            } else {
220                account_locks.readonly.push(key);
221            }
222        }
223
224        account_locks
225    }
226
227    /// Return the list of addresses loaded from on-chain address lookup tables
228    pub fn get_loaded_addresses(&self) -> LoadedAddresses {
229        match &self.message {
230            SanitizedMessage::Legacy(_) => LoadedAddresses::default(),
231            SanitizedMessage::V0(message) => LoadedAddresses::clone(&message.loaded_addresses),
232        }
233    }
234
235    /// If the transaction uses a durable nonce, return the pubkey of the nonce account
236    pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
237        self.message.get_durable_nonce()
238    }
239
240    /// Return the serialized message data to sign.
241    fn message_data(&self) -> Vec<u8> {
242        match &self.message {
243            SanitizedMessage::Legacy(legacy_message) => legacy_message.message.serialize(),
244            SanitizedMessage::V0(loaded_msg) => loaded_msg.message.serialize(),
245        }
246    }
247
248    /// Verify the transaction signatures
249    pub fn verify(&self) -> Result<()> {
250        let message_bytes = self.message_data();
251        if self
252            .signatures
253            .iter()
254            .zip(self.message.account_keys().iter())
255            .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
256            .any(|verified| !verified)
257        {
258            Err(TransactionError::SignatureFailure)
259        } else {
260            Ok(())
261        }
262    }
263
264    /// Verify the precompiled programs in this transaction
265    pub fn verify_precompiles(&self, feature_set: &feature_set::FeatureSet) -> Result<()> {
266        for (index, (program_id, instruction)) in
267            self.message.program_instructions_iter().enumerate()
268        {
269            verify_if_precompile(
270                program_id,
271                instruction,
272                self.message().instructions(),
273                feature_set,
274            )
275            .map_err(|err| {
276                TransactionError::InstructionError(
277                    index as u8,
278                    InstructionError::Custom(err as u32),
279                )
280            })?;
281        }
282        Ok(())
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    ) -> Result<()> {
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        crate::{
320            reserved_account_keys::ReservedAccountKeys,
321            signer::{keypair::Keypair, Signer},
322        },
323        solana_program::vote::{self, state::Vote},
324    };
325
326    #[test]
327    fn test_try_create_simple_vote_tx() {
328        let bank_hash = Hash::default();
329        let block_hash = Hash::default();
330        let vote_keypair = Keypair::new();
331        let node_keypair = Keypair::new();
332        let auth_keypair = Keypair::new();
333        let votes = Vote::new(vec![1, 2, 3], bank_hash);
334        let vote_ix =
335            vote::instruction::vote(&vote_keypair.pubkey(), &auth_keypair.pubkey(), votes);
336        let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey()));
337        vote_tx.partial_sign(&[&node_keypair], block_hash);
338        vote_tx.partial_sign(&[&auth_keypair], block_hash);
339
340        // single legacy vote ix, 2 signatures
341        {
342            let vote_transaction = SanitizedTransaction::try_create(
343                VersionedTransaction::from(vote_tx.clone()),
344                MessageHash::Compute,
345                None,
346                SimpleAddressLoader::Disabled,
347                &ReservedAccountKeys::empty_key_set(),
348            )
349            .unwrap();
350            assert!(vote_transaction.is_simple_vote_transaction());
351        }
352
353        {
354            // call side says it is not a vote
355            let vote_transaction = SanitizedTransaction::try_create(
356                VersionedTransaction::from(vote_tx.clone()),
357                MessageHash::Compute,
358                Some(false),
359                SimpleAddressLoader::Disabled,
360                &ReservedAccountKeys::empty_key_set(),
361            )
362            .unwrap();
363            assert!(!vote_transaction.is_simple_vote_transaction());
364        }
365
366        // single legacy vote ix, 3 signatures
367        vote_tx.signatures.push(Signature::default());
368        vote_tx.message.header.num_required_signatures = 3;
369        {
370            let vote_transaction = SanitizedTransaction::try_create(
371                VersionedTransaction::from(vote_tx.clone()),
372                MessageHash::Compute,
373                None,
374                SimpleAddressLoader::Disabled,
375                &ReservedAccountKeys::empty_key_set(),
376            )
377            .unwrap();
378            assert!(!vote_transaction.is_simple_vote_transaction());
379        }
380
381        {
382            // call site says it is simple vote
383            let vote_transaction = SanitizedTransaction::try_create(
384                VersionedTransaction::from(vote_tx),
385                MessageHash::Compute,
386                Some(true),
387                SimpleAddressLoader::Disabled,
388                &ReservedAccountKeys::empty_key_set(),
389            )
390            .unwrap();
391            assert!(vote_transaction.is_simple_vote_transaction());
392        }
393    }
394}