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    /// If the transaction uses a durable nonce, return the pubkey of the nonce account
265    #[cfg(feature = "wincode")]
266    pub fn get_durable_nonce(&self) -> Option<&Address> {
267        self.message.get_durable_nonce()
268    }
269
270    #[cfg(feature = "verify")]
271    /// Return the serialized message data to sign.
272    fn message_data(&self) -> Vec<u8> {
273        match &self.message {
274            SanitizedMessage::Legacy(legacy_message) => legacy_message.message.serialize(),
275            SanitizedMessage::V0(loaded_msg) => loaded_msg.message.serialize(),
276            SanitizedMessage::V1(cached_msg) => cached_msg.message.serialize(),
277        }
278    }
279
280    #[cfg(feature = "verify")]
281    /// Verify the transaction signatures
282    pub fn verify(&self) -> TransactionResult<()> {
283        let message_bytes = self.message_data();
284        if self
285            .signatures
286            .iter()
287            .zip(self.message.account_keys().iter())
288            .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
289            .any(|verified| !verified)
290        {
291            Err(TransactionError::SignatureFailure)
292        } else {
293            Ok(())
294        }
295    }
296
297    /// Validate a transaction message against locked accounts
298    pub fn validate_account_locks(
299        message: &SanitizedMessage,
300        tx_account_lock_limit: usize,
301    ) -> TransactionResult<()> {
302        if message.has_duplicates() {
303            Err(TransactionError::AccountLoadedTwice)
304        } else if message.account_keys().len() > tx_account_lock_limit {
305            Err(TransactionError::TooManyAccountLocks)
306        } else {
307            Ok(())
308        }
309    }
310
311    #[cfg(feature = "dev-context-only-utils")]
312    pub fn new_for_tests(
313        message: SanitizedMessage,
314        signatures: Vec<Signature>,
315        is_simple_vote_tx: bool,
316    ) -> SanitizedTransaction {
317        SanitizedTransaction {
318            message,
319            message_hash: Hash::new_unique(),
320            signatures,
321            is_simple_vote_tx,
322        }
323    }
324}
325
326#[cfg(test)]
327#[allow(clippy::arithmetic_side_effects)]
328mod tests {
329    use {
330        super::*,
331        alloc::vec,
332        solana_instruction::{AccountMeta, Instruction},
333        solana_keypair::Keypair,
334        solana_message::{MessageHeader, SimpleAddressLoader},
335        solana_signer::Signer,
336        solana_vote_interface::{instruction, state::Vote},
337    };
338
339    #[test]
340    fn test_try_create_simple_vote_tx() {
341        let bank_hash = Hash::default();
342        let block_hash = Hash::default();
343        let empty_key_set = HashSet::default();
344        let vote_keypair = Keypair::new();
345        let node_keypair = Keypair::new();
346        let auth_keypair = Keypair::new();
347        let votes = Vote::new(vec![1, 2, 3], bank_hash);
348        let vote_ix = instruction::vote(&vote_keypair.pubkey(), &auth_keypair.pubkey(), votes);
349        let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey()));
350        vote_tx.partial_sign(&[&node_keypair], block_hash);
351        vote_tx.partial_sign(&[&auth_keypair], block_hash);
352
353        // single legacy vote ix, 2 signatures
354        {
355            let vote_transaction = SanitizedTransaction::try_create(
356                VersionedTransaction::from(vote_tx.clone()),
357                MessageHash::Compute,
358                None,
359                SimpleAddressLoader::Disabled,
360                &empty_key_set,
361            )
362            .unwrap();
363            assert!(vote_transaction.is_simple_vote_transaction());
364        }
365
366        {
367            // call side says it is not a vote
368            let vote_transaction = SanitizedTransaction::try_create(
369                VersionedTransaction::from(vote_tx.clone()),
370                MessageHash::Compute,
371                Some(false),
372                SimpleAddressLoader::Disabled,
373                &empty_key_set,
374            )
375            .unwrap();
376            assert!(!vote_transaction.is_simple_vote_transaction());
377        }
378
379        // single legacy vote ix, 3 signatures
380        vote_tx.signatures.push(Signature::default());
381        vote_tx.message.header.num_required_signatures = 3;
382        {
383            let vote_transaction = SanitizedTransaction::try_create(
384                VersionedTransaction::from(vote_tx.clone()),
385                MessageHash::Compute,
386                None,
387                SimpleAddressLoader::Disabled,
388                &empty_key_set,
389            )
390            .unwrap();
391            assert!(!vote_transaction.is_simple_vote_transaction());
392        }
393
394        {
395            // call site says it is simple vote
396            let vote_transaction = SanitizedTransaction::try_create(
397                VersionedTransaction::from(vote_tx),
398                MessageHash::Compute,
399                Some(true),
400                SimpleAddressLoader::Disabled,
401                &empty_key_set,
402            )
403            .unwrap();
404            assert!(vote_transaction.is_simple_vote_transaction());
405        }
406    }
407
408    #[test]
409    fn test_try_new_from_fields() {
410        let legacy_message = SanitizedMessage::try_from_legacy_message(
411            legacy::Message {
412                header: MessageHeader {
413                    num_required_signatures: 2,
414                    num_readonly_signed_accounts: 1,
415                    num_readonly_unsigned_accounts: 1,
416                },
417                account_keys: vec![
418                    Address::new_unique(),
419                    Address::new_unique(),
420                    Address::new_unique(),
421                ],
422                ..legacy::Message::default()
423            },
424            &HashSet::default(),
425        )
426        .unwrap();
427
428        for is_simple_vote_tx in [false, true] {
429            // Not enough signatures
430            assert!(SanitizedTransaction::try_new_from_fields(
431                legacy_message.clone(),
432                Hash::new_unique(),
433                is_simple_vote_tx,
434                vec![],
435            )
436            .is_err());
437            // Too many signatures
438            assert!(SanitizedTransaction::try_new_from_fields(
439                legacy_message.clone(),
440                Hash::new_unique(),
441                is_simple_vote_tx,
442                vec![
443                    Signature::default(),
444                    Signature::default(),
445                    Signature::default()
446                ],
447            )
448            .is_err());
449            // Correct number of signatures.
450            assert!(SanitizedTransaction::try_new_from_fields(
451                legacy_message.clone(),
452                Hash::new_unique(),
453                is_simple_vote_tx,
454                vec![Signature::default(), Signature::default()]
455            )
456            .is_ok());
457        }
458    }
459
460    #[test]
461    fn test_verify_v1_message_data_includes_prefix() {
462        let payer = Keypair::new();
463        let program_id = Address::new_unique();
464        let instruction = Instruction::new_with_bytes(
465            program_id,
466            &[1, 2, 3],
467            vec![AccountMeta::new(payer.pubkey(), true)],
468        );
469        let message =
470            v1::Message::try_compile(&payer.pubkey(), &[instruction], Hash::new_unique()).unwrap();
471        let versioned_tx =
472            VersionedTransaction::try_new(VersionedMessage::V1(message), &[&payer]).unwrap();
473
474        let sanitized = SanitizedTransaction::try_create(
475            versioned_tx,
476            MessageHash::Compute,
477            None,
478            SimpleAddressLoader::Disabled,
479            &HashSet::default(),
480        )
481        .unwrap();
482
483        // The signed bytes must begin with the V1 version prefix.
484        let signed_bytes = sanitized.message_data();
485        assert_eq!(signed_bytes[0], solana_message::v1::V1_PREFIX);
486
487        sanitized.verify().unwrap();
488    }
489}