clone_solana_transaction/
sanitized.rs

1use {
2    crate::versioned::{sanitized::SanitizedVersionedTransaction, VersionedTransaction},
3    clone_solana_hash::Hash,
4    clone_solana_message::{
5        legacy,
6        v0::{self, LoadedAddresses},
7        AddressLoader, LegacyMessage, SanitizedMessage, SanitizedVersionedMessage,
8        VersionedMessage,
9    },
10    clone_solana_pubkey::Pubkey,
11    clone_solana_signature::Signature,
12    clone_solana_transaction_error::{TransactionError, TransactionResult as Result},
13    std::collections::HashSet,
14};
15#[cfg(feature = "blake3")]
16use {crate::Transaction, clone_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    #[cfg(feature = "precompiles")]
288    /// Verify the precompiled programs in this transaction
289    pub fn verify_precompiles(
290        &self,
291        feature_set: &clone_solana_feature_set::FeatureSet,
292    ) -> Result<()> {
293        for (index, (program_id, instruction)) in
294            self.message.program_instructions_iter().enumerate()
295        {
296            clone_solana_precompiles::verify_if_precompile(
297                program_id,
298                instruction,
299                self.message().instructions(),
300                feature_set,
301            )
302            .map_err(|err| {
303                TransactionError::InstructionError(
304                    index as u8,
305                    clone_solana_instruction::error::InstructionError::Custom(err as u32),
306                )
307            })?;
308        }
309        Ok(())
310    }
311
312    /// Validate a transaction message against locked accounts
313    pub fn validate_account_locks(
314        message: &SanitizedMessage,
315        tx_account_lock_limit: usize,
316    ) -> Result<()> {
317        if message.has_duplicates() {
318            Err(TransactionError::AccountLoadedTwice)
319        } else if message.account_keys().len() > tx_account_lock_limit {
320            Err(TransactionError::TooManyAccountLocks)
321        } else {
322            Ok(())
323        }
324    }
325
326    #[cfg(feature = "dev-context-only-utils")]
327    pub fn new_for_tests(
328        message: SanitizedMessage,
329        signatures: Vec<Signature>,
330        is_simple_vote_tx: bool,
331    ) -> SanitizedTransaction {
332        SanitizedTransaction {
333            message,
334            message_hash: Hash::new_unique(),
335            signatures,
336            is_simple_vote_tx,
337        }
338    }
339}
340
341#[cfg(test)]
342#[allow(clippy::arithmetic_side_effects)]
343mod tests {
344    use {
345        super::*,
346        clone_solana_keypair::Keypair,
347        clone_solana_message::{MessageHeader, SimpleAddressLoader},
348        clone_solana_program::vote::{self, state::Vote},
349        clone_solana_signer::Signer,
350    };
351
352    #[test]
353    fn test_try_create_simple_vote_tx() {
354        let bank_hash = Hash::default();
355        let block_hash = Hash::default();
356        let empty_key_set = HashSet::default();
357        let vote_keypair = Keypair::new();
358        let node_keypair = Keypair::new();
359        let auth_keypair = Keypair::new();
360        let votes = Vote::new(vec![1, 2, 3], bank_hash);
361        let vote_ix =
362            vote::instruction::vote(&vote_keypair.pubkey(), &auth_keypair.pubkey(), votes);
363        let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey()));
364        vote_tx.partial_sign(&[&node_keypair], block_hash);
365        vote_tx.partial_sign(&[&auth_keypair], block_hash);
366
367        // single legacy vote ix, 2 signatures
368        {
369            let vote_transaction = SanitizedTransaction::try_create(
370                VersionedTransaction::from(vote_tx.clone()),
371                MessageHash::Compute,
372                None,
373                SimpleAddressLoader::Disabled,
374                &empty_key_set,
375            )
376            .unwrap();
377            assert!(vote_transaction.is_simple_vote_transaction());
378        }
379
380        {
381            // call side says it is not a vote
382            let vote_transaction = SanitizedTransaction::try_create(
383                VersionedTransaction::from(vote_tx.clone()),
384                MessageHash::Compute,
385                Some(false),
386                SimpleAddressLoader::Disabled,
387                &empty_key_set,
388            )
389            .unwrap();
390            assert!(!vote_transaction.is_simple_vote_transaction());
391        }
392
393        // single legacy vote ix, 3 signatures
394        vote_tx.signatures.push(Signature::default());
395        vote_tx.message.header.num_required_signatures = 3;
396        {
397            let vote_transaction = SanitizedTransaction::try_create(
398                VersionedTransaction::from(vote_tx.clone()),
399                MessageHash::Compute,
400                None,
401                SimpleAddressLoader::Disabled,
402                &empty_key_set,
403            )
404            .unwrap();
405            assert!(!vote_transaction.is_simple_vote_transaction());
406        }
407
408        {
409            // call site says it is simple vote
410            let vote_transaction = SanitizedTransaction::try_create(
411                VersionedTransaction::from(vote_tx),
412                MessageHash::Compute,
413                Some(true),
414                SimpleAddressLoader::Disabled,
415                &empty_key_set,
416            )
417            .unwrap();
418            assert!(vote_transaction.is_simple_vote_transaction());
419        }
420    }
421
422    #[test]
423    fn test_try_new_from_fields() {
424        let legacy_message = SanitizedMessage::try_from_legacy_message(
425            legacy::Message {
426                header: MessageHeader {
427                    num_required_signatures: 2,
428                    num_readonly_signed_accounts: 1,
429                    num_readonly_unsigned_accounts: 1,
430                },
431                account_keys: vec![
432                    Pubkey::new_unique(),
433                    Pubkey::new_unique(),
434                    Pubkey::new_unique(),
435                ],
436                ..legacy::Message::default()
437            },
438            &HashSet::default(),
439        )
440        .unwrap();
441
442        for is_simple_vote_tx in [false, true] {
443            // Not enough signatures
444            assert!(SanitizedTransaction::try_new_from_fields(
445                legacy_message.clone(),
446                Hash::new_unique(),
447                is_simple_vote_tx,
448                vec![],
449            )
450            .is_err());
451            // Too many signatures
452            assert!(SanitizedTransaction::try_new_from_fields(
453                legacy_message.clone(),
454                Hash::new_unique(),
455                is_simple_vote_tx,
456                vec![
457                    Signature::default(),
458                    Signature::default(),
459                    Signature::default()
460                ],
461            )
462            .is_err());
463            // Correct number of signatures.
464            assert!(SanitizedTransaction::try_new_from_fields(
465                legacy_message.clone(),
466                Hash::new_unique(),
467                is_simple_vote_tx,
468                vec![Signature::default(), Signature::default()]
469            )
470            .is_ok());
471        }
472    }
473}