solana_runtime/
account_rent_state.rs

1use {
2    log::*,
3    solana_sdk::{
4        account::{AccountSharedData, ReadableAccount},
5        pubkey::Pubkey,
6        rent::Rent,
7        transaction::{Result, TransactionError},
8        transaction_context::TransactionContext,
9    },
10};
11
12#[derive(Debug, PartialEq, Eq)]
13pub(crate) enum RentState {
14    /// account.lamports == 0
15    Uninitialized,
16    /// 0 < account.lamports < rent-exempt-minimum
17    RentPaying {
18        lamports: u64,    // account.lamports()
19        data_size: usize, // account.data().len()
20    },
21    /// account.lamports >= rent-exempt-minimum
22    RentExempt,
23}
24
25impl RentState {
26    pub(crate) fn from_account(account: &AccountSharedData, rent: &Rent) -> Self {
27        if account.lamports() == 0 {
28            Self::Uninitialized
29        } else if rent.is_exempt(account.lamports(), account.data().len()) {
30            Self::RentExempt
31        } else {
32            Self::RentPaying {
33                data_size: account.data().len(),
34                lamports: account.lamports(),
35            }
36        }
37    }
38
39    pub(crate) fn transition_allowed_from(
40        &self,
41        pre_rent_state: &RentState,
42        prevent_crediting_accounts_that_end_rent_paying: bool,
43    ) -> bool {
44        match self {
45            Self::Uninitialized | Self::RentExempt => true,
46            Self::RentPaying {
47                data_size: post_data_size,
48                lamports: post_lamports,
49            } => {
50                match pre_rent_state {
51                    Self::Uninitialized | Self::RentExempt => false,
52                    Self::RentPaying {
53                        data_size: pre_data_size,
54                        lamports: pre_lamports,
55                    } => {
56                        // Cannot remain RentPaying if resized
57                        if post_data_size != pre_data_size {
58                            false
59                        } else if prevent_crediting_accounts_that_end_rent_paying {
60                            // Cannot remain RentPaying if credited
61                            post_lamports <= pre_lamports
62                        } else {
63                            true
64                        }
65                    }
66                }
67            }
68        }
69    }
70}
71
72pub(crate) fn submit_rent_state_metrics(pre_rent_state: &RentState, post_rent_state: &RentState) {
73    match (pre_rent_state, post_rent_state) {
74        (&RentState::Uninitialized, &RentState::RentPaying { .. }) => {
75            inc_new_counter_info!("rent_paying_err-new_account", 1);
76        }
77        (&RentState::RentPaying { .. }, &RentState::RentPaying { .. }) => {
78            inc_new_counter_info!("rent_paying_ok-legacy", 1);
79        }
80        (_, &RentState::RentPaying { .. }) => {
81            inc_new_counter_info!("rent_paying_err-other", 1);
82        }
83        _ => {}
84    }
85}
86
87pub(crate) fn check_rent_state(
88    pre_rent_state: Option<&RentState>,
89    post_rent_state: Option<&RentState>,
90    transaction_context: &TransactionContext,
91    index: usize,
92    include_account_index_in_err: bool,
93    prevent_crediting_accounts_that_end_rent_paying: bool,
94) -> Result<()> {
95    if let Some((pre_rent_state, post_rent_state)) = pre_rent_state.zip(post_rent_state) {
96        let expect_msg = "account must exist at TransactionContext index if rent-states are Some";
97        check_rent_state_with_account(
98            pre_rent_state,
99            post_rent_state,
100            transaction_context
101                .get_key_of_account_at_index(index)
102                .expect(expect_msg),
103            &transaction_context
104                .get_account_at_index(index)
105                .expect(expect_msg)
106                .borrow(),
107            include_account_index_in_err.then(|| index),
108            prevent_crediting_accounts_that_end_rent_paying,
109        )?;
110    }
111    Ok(())
112}
113
114pub(crate) fn check_rent_state_with_account(
115    pre_rent_state: &RentState,
116    post_rent_state: &RentState,
117    address: &Pubkey,
118    account_state: &AccountSharedData,
119    account_index: Option<usize>,
120    prevent_crediting_accounts_that_end_rent_paying: bool,
121) -> Result<()> {
122    submit_rent_state_metrics(pre_rent_state, post_rent_state);
123    if !solana_sdk::incinerator::check_id(address)
124        && !post_rent_state.transition_allowed_from(
125            pre_rent_state,
126            prevent_crediting_accounts_that_end_rent_paying,
127        )
128    {
129        debug!(
130            "Account {} not rent exempt, state {:?}",
131            address, account_state,
132        );
133        if let Some(account_index) = account_index {
134            let account_index = account_index as u8;
135            Err(TransactionError::InsufficientFundsForRent { account_index })
136        } else {
137            Err(TransactionError::InvalidRentPayingAccount)
138        }
139    } else {
140        Ok(())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use {super::*, solana_sdk::pubkey::Pubkey};
147
148    #[test]
149    fn test_from_account() {
150        let program_id = Pubkey::new_unique();
151        let uninitialized_account = AccountSharedData::new(0, 0, &Pubkey::default());
152
153        let account_data_size = 100;
154
155        let rent = Rent::free();
156        let rent_exempt_account = AccountSharedData::new(1, account_data_size, &program_id); // if rent is free, all accounts with non-zero lamports and non-empty data are rent-exempt
157
158        assert_eq!(
159            RentState::from_account(&uninitialized_account, &rent),
160            RentState::Uninitialized
161        );
162        assert_eq!(
163            RentState::from_account(&rent_exempt_account, &rent),
164            RentState::RentExempt
165        );
166
167        let rent = Rent::default();
168        let rent_minimum_balance = rent.minimum_balance(account_data_size);
169        let rent_paying_account = AccountSharedData::new(
170            rent_minimum_balance.saturating_sub(1),
171            account_data_size,
172            &program_id,
173        );
174        let rent_exempt_account = AccountSharedData::new(
175            rent.minimum_balance(account_data_size),
176            account_data_size,
177            &program_id,
178        );
179
180        assert_eq!(
181            RentState::from_account(&uninitialized_account, &rent),
182            RentState::Uninitialized
183        );
184        assert_eq!(
185            RentState::from_account(&rent_paying_account, &rent),
186            RentState::RentPaying {
187                data_size: account_data_size,
188                lamports: rent_paying_account.lamports(),
189            }
190        );
191        assert_eq!(
192            RentState::from_account(&rent_exempt_account, &rent),
193            RentState::RentExempt
194        );
195    }
196
197    #[test]
198    fn test_transition_allowed_from() {
199        check_transition_allowed_from(
200            /*prevent_crediting_accounts_that_end_rent_paying:*/ false,
201        );
202        check_transition_allowed_from(
203            /*prevent_crediting_accounts_that_end_rent_paying:*/ true,
204        );
205    }
206
207    fn check_transition_allowed_from(prevent_crediting_accounts_that_end_rent_paying: bool) {
208        let post_rent_state = RentState::Uninitialized;
209        assert!(post_rent_state.transition_allowed_from(
210            &RentState::Uninitialized,
211            prevent_crediting_accounts_that_end_rent_paying
212        ));
213        assert!(post_rent_state.transition_allowed_from(
214            &RentState::RentExempt,
215            prevent_crediting_accounts_that_end_rent_paying
216        ));
217        assert!(post_rent_state.transition_allowed_from(
218            &RentState::RentPaying {
219                data_size: 0,
220                lamports: 1,
221            },
222            prevent_crediting_accounts_that_end_rent_paying
223        ));
224
225        let post_rent_state = RentState::RentExempt;
226        assert!(post_rent_state.transition_allowed_from(
227            &RentState::Uninitialized,
228            prevent_crediting_accounts_that_end_rent_paying
229        ));
230        assert!(post_rent_state.transition_allowed_from(
231            &RentState::RentExempt,
232            prevent_crediting_accounts_that_end_rent_paying
233        ));
234        assert!(post_rent_state.transition_allowed_from(
235            &RentState::RentPaying {
236                data_size: 0,
237                lamports: 1,
238            },
239            prevent_crediting_accounts_that_end_rent_paying
240        ));
241        let post_rent_state = RentState::RentPaying {
242            data_size: 2,
243            lamports: 5,
244        };
245        assert!(!post_rent_state.transition_allowed_from(
246            &RentState::Uninitialized,
247            prevent_crediting_accounts_that_end_rent_paying
248        ));
249        assert!(!post_rent_state.transition_allowed_from(
250            &RentState::RentExempt,
251            prevent_crediting_accounts_that_end_rent_paying
252        ));
253        assert!(!post_rent_state.transition_allowed_from(
254            &RentState::RentPaying {
255                data_size: 3,
256                lamports: 5
257            },
258            prevent_crediting_accounts_that_end_rent_paying
259        ));
260        assert!(!post_rent_state.transition_allowed_from(
261            &RentState::RentPaying {
262                data_size: 1,
263                lamports: 5
264            },
265            prevent_crediting_accounts_that_end_rent_paying
266        ));
267        // Transition is always allowed if there is no account data resize or
268        // change in account's lamports.
269        assert!(post_rent_state.transition_allowed_from(
270            &RentState::RentPaying {
271                data_size: 2,
272                lamports: 5
273            },
274            prevent_crediting_accounts_that_end_rent_paying
275        ));
276        // Transition is always allowed if there is no account data resize and
277        // account's lamports is reduced.
278        assert!(post_rent_state.transition_allowed_from(
279            &RentState::RentPaying {
280                data_size: 2,
281                lamports: 7
282            },
283            prevent_crediting_accounts_that_end_rent_paying
284        ));
285        // Once the feature is activated, transition is not allowed if the
286        // account is credited with more lamports and remains rent-paying.
287        assert_eq!(
288            post_rent_state.transition_allowed_from(
289                &RentState::RentPaying {
290                    data_size: 2,
291                    lamports: 3
292                },
293                prevent_crediting_accounts_that_end_rent_paying
294            ),
295            !prevent_crediting_accounts_that_end_rent_paying
296        );
297    }
298}