atlas_runtime/
transaction_batch.rs

1use {
2    crate::bank::Bank, core::ops::Deref, solana_svm_transaction::svm_message::SVMMessage,
3    solana_transaction_error::TransactionResult as Result,
4};
5
6pub enum OwnedOrBorrowed<'a, T> {
7    Owned(Vec<T>),
8    Borrowed(&'a [T]),
9}
10
11impl<T> Deref for OwnedOrBorrowed<'_, T> {
12    type Target = [T];
13
14    fn deref(&self) -> &Self::Target {
15        match self {
16            OwnedOrBorrowed::Owned(v) => v,
17            OwnedOrBorrowed::Borrowed(v) => v,
18        }
19    }
20}
21
22// Represents the results of trying to lock a set of accounts
23pub struct TransactionBatch<'a, 'b, Tx: SVMMessage> {
24    lock_results: Vec<Result<()>>,
25    bank: &'a Bank,
26    sanitized_txs: OwnedOrBorrowed<'b, Tx>,
27    needs_unlock: bool,
28}
29
30impl<'a, 'b, Tx: SVMMessage> TransactionBatch<'a, 'b, Tx> {
31    pub fn new(
32        lock_results: Vec<Result<()>>,
33        bank: &'a Bank,
34        sanitized_txs: OwnedOrBorrowed<'b, Tx>,
35    ) -> Self {
36        assert_eq!(lock_results.len(), sanitized_txs.len());
37        Self {
38            lock_results,
39            bank,
40            sanitized_txs,
41            needs_unlock: true,
42        }
43    }
44
45    pub fn lock_results(&self) -> &Vec<Result<()>> {
46        &self.lock_results
47    }
48
49    pub fn sanitized_transactions(&self) -> &[Tx] {
50        &self.sanitized_txs
51    }
52
53    pub fn bank(&self) -> &Bank {
54        self.bank
55    }
56
57    pub fn set_needs_unlock(&mut self, needs_unlock: bool) {
58        self.needs_unlock = needs_unlock;
59    }
60
61    pub fn needs_unlock(&self) -> bool {
62        self.needs_unlock
63    }
64
65    /// For every error result, if the corresponding transaction is
66    /// still locked, unlock the transaction and then record the new error.
67    pub fn unlock_failures(&mut self, transaction_results: Vec<Result<()>>) {
68        assert_eq!(self.lock_results.len(), transaction_results.len());
69        // Shouldn't happen but if a batch was marked as not needing an unlock,
70        // don't unlock failures.
71        if !self.needs_unlock() {
72            return;
73        }
74
75        let txs_and_results = transaction_results
76            .iter()
77            .enumerate()
78            .inspect(|(index, result)| {
79                // It's not valid to update a previously recorded lock error to
80                // become an "ok" result because this could lead to serious
81                // account lock violations where accounts are later unlocked
82                // when they were not currently locked.
83                assert!(!(result.is_ok() && self.lock_results[*index].is_err()))
84            })
85            .filter(|(index, result)| result.is_err() && self.lock_results[*index].is_ok())
86            .map(|(index, _)| (&self.sanitized_txs[index], &self.lock_results[index]));
87
88        // Unlock the accounts for all transactions which will be updated to an
89        // lock error below.
90        self.bank.unlock_accounts(txs_and_results);
91
92        // Record all new errors by overwriting lock results. Note that it's
93        // not valid to update from err -> ok and the assertion above enforces
94        // that validity constraint.
95        self.lock_results = transaction_results;
96    }
97}
98
99// Unlock all locked accounts in destructor.
100impl<Tx: SVMMessage> Drop for TransactionBatch<'_, '_, Tx> {
101    fn drop(&mut self) {
102        if self.needs_unlock() {
103            self.set_needs_unlock(false);
104            self.bank.unlock_accounts(
105                self.sanitized_transactions()
106                    .iter()
107                    .zip(self.lock_results()),
108            )
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use {
116        super::*,
117        crate::genesis_utils::{create_genesis_config_with_leader, GenesisConfigInfo},
118        solana_keypair::Keypair,
119        solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
120        solana_system_transaction as system_transaction,
121        solana_transaction::sanitized::SanitizedTransaction,
122        solana_transaction_error::TransactionError,
123        test_case::test_case,
124    };
125
126    #[test_case(false; "old")]
127    #[test_case(true; "simd83")]
128    fn test_transaction_batch(relax_intrabatch_account_locks: bool) {
129        let (bank, txs) = setup(false, relax_intrabatch_account_locks);
130
131        // Test getting locked accounts
132        let batch = bank.prepare_sanitized_batch(&txs);
133
134        // Grab locks
135        assert!(batch.lock_results().iter().all(|x| x.is_ok()));
136
137        // Trying to grab locks again should fail
138        let batch2 = bank.prepare_sanitized_batch(&txs);
139        assert!(batch2.lock_results().iter().all(|x| x.is_err()));
140
141        // Drop the first set of locks
142        drop(batch);
143
144        // Now grabbing locks should work again
145        let batch2 = bank.prepare_sanitized_batch(&txs);
146        assert!(batch2.lock_results().iter().all(|x| x.is_ok()));
147    }
148
149    #[test_case(false; "old")]
150    #[test_case(true; "simd83")]
151    fn test_simulation_batch(relax_intrabatch_account_locks: bool) {
152        let (bank, txs) = setup(false, relax_intrabatch_account_locks);
153
154        // Prepare batch without locks
155        let batch = bank.prepare_unlocked_batch_from_single_tx(&txs[0]);
156        assert!(batch.lock_results().iter().all(|x| x.is_ok()));
157
158        // Grab locks
159        let batch2 = bank.prepare_sanitized_batch(&txs);
160        assert!(batch2.lock_results().iter().all(|x| x.is_ok()));
161
162        // Prepare another batch without locks
163        let batch3 = bank.prepare_unlocked_batch_from_single_tx(&txs[0]);
164        assert!(batch3.lock_results().iter().all(|x| x.is_ok()));
165    }
166
167    #[test_case(false; "old")]
168    #[test_case(true; "simd83")]
169    fn test_unlock_failures(relax_intrabatch_account_locks: bool) {
170        let (bank, txs) = setup(true, relax_intrabatch_account_locks);
171        let expected_lock_results = if relax_intrabatch_account_locks {
172            vec![Ok(()), Ok(()), Ok(())]
173        } else {
174            vec![Ok(()), Err(TransactionError::AccountInUse), Ok(())]
175        };
176
177        // Test getting locked accounts
178        let mut batch = bank.prepare_sanitized_batch(&txs);
179        assert_eq!(batch.lock_results, expected_lock_results,);
180
181        let qos_results = vec![
182            Ok(()),
183            Err(TransactionError::WouldExceedMaxBlockCostLimit),
184            Err(TransactionError::WouldExceedMaxBlockCostLimit),
185        ];
186        batch.unlock_failures(qos_results.clone());
187        assert_eq!(batch.lock_results, qos_results);
188
189        // Dropping the batch should unlock remaining locked transactions
190        drop(batch);
191
192        // The next batch should be able to take all the same locks as before
193        let batch2 = bank.prepare_sanitized_batch(&txs);
194        assert_eq!(batch2.lock_results, expected_lock_results,);
195    }
196
197    fn setup(
198        insert_conflicting_tx: bool,
199        relax_intrabatch_account_locks: bool,
200    ) -> (Bank, Vec<RuntimeTransaction<SanitizedTransaction>>) {
201        let dummy_leader_pubkey = solana_pubkey::new_rand();
202        let GenesisConfigInfo {
203            genesis_config,
204            mint_keypair,
205            ..
206        } = create_genesis_config_with_leader(500, &dummy_leader_pubkey, 100);
207        let mut bank = Bank::new_for_tests(&genesis_config);
208        if !relax_intrabatch_account_locks {
209            bank.deactivate_feature(&agave_feature_set::relax_intrabatch_account_locks::id());
210        }
211
212        let pubkey = solana_pubkey::new_rand();
213        let keypair2 = Keypair::new();
214        let pubkey2 = solana_pubkey::new_rand();
215
216        let mut txs = vec![RuntimeTransaction::from_transaction_for_tests(
217            system_transaction::transfer(&mint_keypair, &pubkey, 1, genesis_config.hash()),
218        )];
219        if insert_conflicting_tx {
220            txs.push(RuntimeTransaction::from_transaction_for_tests(
221                system_transaction::transfer(&mint_keypair, &pubkey2, 1, genesis_config.hash()),
222            ));
223        }
224        txs.push(RuntimeTransaction::from_transaction_for_tests(
225            system_transaction::transfer(&keypair2, &pubkey2, 1, genesis_config.hash()),
226        ));
227
228        (bank, txs)
229    }
230}