solana_svm/
rollback_accounts.rs

1use {
2    crate::nonce_info::NonceInfo,
3    solana_sdk::{
4        account::{AccountSharedData, ReadableAccount, WritableAccount},
5        clock::Epoch,
6        pubkey::Pubkey,
7    },
8};
9
10/// Captured account state used to rollback account state for nonce and fee
11/// payer accounts after a failed executed transaction.
12#[derive(PartialEq, Eq, Debug, Clone)]
13pub enum RollbackAccounts {
14    FeePayerOnly {
15        fee_payer_account: AccountSharedData,
16    },
17    SameNonceAndFeePayer {
18        nonce: NonceInfo,
19    },
20    SeparateNonceAndFeePayer {
21        nonce: NonceInfo,
22        fee_payer_account: AccountSharedData,
23    },
24}
25
26#[cfg(feature = "dev-context-only-utils")]
27impl Default for RollbackAccounts {
28    fn default() -> Self {
29        Self::FeePayerOnly {
30            fee_payer_account: AccountSharedData::default(),
31        }
32    }
33}
34
35impl RollbackAccounts {
36    pub fn new(
37        nonce: Option<NonceInfo>,
38        fee_payer_address: Pubkey,
39        mut fee_payer_account: AccountSharedData,
40        fee_payer_rent_debit: u64,
41        fee_payer_loaded_rent_epoch: Epoch,
42    ) -> Self {
43        // When the fee payer account is rolled back due to transaction failure,
44        // rent should not be charged so credit the previously debited rent
45        // amount.
46        fee_payer_account.set_lamports(
47            fee_payer_account
48                .lamports()
49                .saturating_add(fee_payer_rent_debit),
50        );
51
52        if let Some(nonce) = nonce {
53            if &fee_payer_address == nonce.address() {
54                // `nonce` contains an AccountSharedData which has already been advanced to the current DurableNonce
55                // `fee_payer_account` is an AccountSharedData as it currently exists on-chain
56                // thus if the nonce account is being used as the fee payer, we need to update that data here
57                // so we capture both the data change for the nonce and the lamports/rent epoch change for the fee payer
58                fee_payer_account.set_data_from_slice(nonce.account().data());
59
60                RollbackAccounts::SameNonceAndFeePayer {
61                    nonce: NonceInfo::new(fee_payer_address, fee_payer_account),
62                }
63            } else {
64                RollbackAccounts::SeparateNonceAndFeePayer {
65                    nonce,
66                    fee_payer_account,
67                }
68            }
69        } else {
70            // When rolling back failed transactions which don't use nonces, the
71            // runtime should not update the fee payer's rent epoch so reset the
72            // rollback fee payer account's rent epoch to its originally loaded
73            // rent epoch value. In the future, a feature gate could be used to
74            // alter this behavior such that rent epoch updates are handled the
75            // same for both nonce and non-nonce failed transactions.
76            fee_payer_account.set_rent_epoch(fee_payer_loaded_rent_epoch);
77            RollbackAccounts::FeePayerOnly { fee_payer_account }
78        }
79    }
80
81    /// Number of accounts tracked for rollback
82    pub fn count(&self) -> usize {
83        match self {
84            Self::FeePayerOnly { .. } | Self::SameNonceAndFeePayer { .. } => 1,
85            Self::SeparateNonceAndFeePayer { .. } => 2,
86        }
87    }
88
89    /// Size of accounts tracked for rollback, used when calculating the actual
90    /// cost of transaction processing in the cost model.
91    pub fn data_size(&self) -> usize {
92        match self {
93            Self::FeePayerOnly { fee_payer_account } => fee_payer_account.data().len(),
94            Self::SameNonceAndFeePayer { nonce } => nonce.account().data().len(),
95            Self::SeparateNonceAndFeePayer {
96                nonce,
97                fee_payer_account,
98            } => fee_payer_account
99                .data()
100                .len()
101                .saturating_add(nonce.account().data().len()),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use {
109        super::*,
110        solana_sdk::{
111            account::{ReadableAccount, WritableAccount},
112            hash::Hash,
113            nonce::state::{
114                Data as NonceData, DurableNonce, State as NonceState, Versions as NonceVersions,
115            },
116            system_program,
117        },
118    };
119
120    #[test]
121    fn test_new_fee_payer_only() {
122        let fee_payer_address = Pubkey::new_unique();
123        let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default());
124        let fee_payer_rent_epoch = fee_payer_account.rent_epoch();
125
126        const TEST_RENT_DEBIT: u64 = 1;
127        let rent_collected_fee_payer_account = {
128            let mut account = fee_payer_account.clone();
129            account.set_lamports(fee_payer_account.lamports() - TEST_RENT_DEBIT);
130            account.set_rent_epoch(fee_payer_rent_epoch + 1);
131            account
132        };
133
134        let rollback_accounts = RollbackAccounts::new(
135            None,
136            fee_payer_address,
137            rent_collected_fee_payer_account,
138            TEST_RENT_DEBIT,
139            fee_payer_rent_epoch,
140        );
141
142        let expected_fee_payer_account = fee_payer_account;
143        match rollback_accounts {
144            RollbackAccounts::FeePayerOnly { fee_payer_account } => {
145                assert_eq!(expected_fee_payer_account, fee_payer_account);
146            }
147            _ => panic!("Expected FeePayerOnly variant"),
148        }
149    }
150
151    #[test]
152    fn test_new_same_nonce_and_fee_payer() {
153        let nonce_address = Pubkey::new_unique();
154        let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
155        let lamports_per_signature = 42;
156        let nonce_account = AccountSharedData::new_data(
157            43,
158            &NonceVersions::new(NonceState::Initialized(NonceData::new(
159                Pubkey::default(),
160                durable_nonce,
161                lamports_per_signature,
162            ))),
163            &system_program::id(),
164        )
165        .unwrap();
166
167        const TEST_RENT_DEBIT: u64 = 1;
168        let rent_collected_nonce_account = {
169            let mut account = nonce_account.clone();
170            account.set_lamports(nonce_account.lamports() - TEST_RENT_DEBIT);
171            account
172        };
173
174        let nonce = NonceInfo::new(nonce_address, rent_collected_nonce_account.clone());
175        let rollback_accounts = RollbackAccounts::new(
176            Some(nonce),
177            nonce_address,
178            rent_collected_nonce_account,
179            TEST_RENT_DEBIT,
180            u64::MAX, // ignored
181        );
182
183        match rollback_accounts {
184            RollbackAccounts::SameNonceAndFeePayer { nonce } => {
185                assert_eq!(nonce.address(), &nonce_address);
186                assert_eq!(nonce.account(), &nonce_account);
187            }
188            _ => panic!("Expected SameNonceAndFeePayer variant"),
189        }
190    }
191
192    #[test]
193    fn test_separate_nonce_and_fee_payer() {
194        let nonce_address = Pubkey::new_unique();
195        let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
196        let lamports_per_signature = 42;
197        let nonce_account = AccountSharedData::new_data(
198            43,
199            &NonceVersions::new(NonceState::Initialized(NonceData::new(
200                Pubkey::default(),
201                durable_nonce,
202                lamports_per_signature,
203            ))),
204            &system_program::id(),
205        )
206        .unwrap();
207
208        let fee_payer_address = Pubkey::new_unique();
209        let fee_payer_account = AccountSharedData::new(44, 0, &Pubkey::default());
210
211        const TEST_RENT_DEBIT: u64 = 1;
212        let rent_collected_fee_payer_account = {
213            let mut account = fee_payer_account.clone();
214            account.set_lamports(fee_payer_account.lamports() - TEST_RENT_DEBIT);
215            account
216        };
217
218        let nonce = NonceInfo::new(nonce_address, nonce_account.clone());
219        let rollback_accounts = RollbackAccounts::new(
220            Some(nonce),
221            fee_payer_address,
222            rent_collected_fee_payer_account.clone(),
223            TEST_RENT_DEBIT,
224            u64::MAX, // ignored
225        );
226
227        let expected_fee_payer_account = fee_payer_account;
228        match rollback_accounts {
229            RollbackAccounts::SeparateNonceAndFeePayer {
230                nonce,
231                fee_payer_account,
232            } => {
233                assert_eq!(nonce.address(), &nonce_address);
234                assert_eq!(nonce.account(), &nonce_account);
235                assert_eq!(expected_fee_payer_account, fee_payer_account);
236            }
237            _ => panic!("Expected SeparateNonceAndFeePayer variant"),
238        }
239    }
240}