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 Uninitialized,
16 RentPaying {
18 lamports: u64, data_size: usize, },
21 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 if post_data_size != pre_data_size {
58 false
59 } else if prevent_crediting_accounts_that_end_rent_paying {
60 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); 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 false,
201 );
202 check_transition_allowed_from(
203 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 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 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 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}