Skip to main content

solana_transaction/
sanitized.rs

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