diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs index 6df6a4b059501e..810b84ba18c41a 100644 --- a/runtime/src/account_rent_state.rs +++ b/runtime/src/account_rent_state.rs @@ -14,8 +14,10 @@ pub(crate) enum RentState { /// account.lamports == 0 Uninitialized, /// 0 < account.lamports < rent-exempt-minimum - /// Parameter is the size of the account data - RentPaying(usize), + RentPaying { + lamports: u64, // account.lamports() + data_size: usize, // account.data().len() + }, /// account.lamports >= rent-exempt-minimum RentExempt, } @@ -24,10 +26,13 @@ impl RentState { pub(crate) fn from_account(account: &AccountSharedData, rent: &Rent) -> Self { if account.lamports() == 0 { Self::Uninitialized - } else if !rent.is_exempt(account.lamports(), account.data().len()) { - Self::RentPaying(account.data().len()) - } else { + } else if rent.is_exempt(account.lamports(), account.data().len()) { Self::RentExempt + } else { + Self::RentPaying { + data_size: account.data().len(), + lamports: account.lamports(), + } } } @@ -35,32 +40,45 @@ impl RentState { &self, pre_rent_state: &RentState, do_support_realloc: bool, + prevent_crediting_accounts_that_end_rent_paying: bool, ) -> bool { - if let Self::RentPaying(post_data_size) = self { - if let Self::RentPaying(pre_data_size) = pre_rent_state { - if do_support_realloc { - post_data_size == pre_data_size // Cannot be RentPaying if resized - } else { - true // RentPaying can continue to be RentPaying + match self { + Self::Uninitialized | Self::RentExempt => true, + Self::RentPaying { + data_size: post_data_size, + lamports: post_lamports, + } => { + match pre_rent_state { + Self::Uninitialized | Self::RentExempt => false, + Self::RentPaying { + data_size: pre_data_size, + lamports: pre_lamports, + } => { + // Cannot remain RentPaying if resized + if do_support_realloc && post_data_size != pre_data_size { + false + } else if prevent_crediting_accounts_that_end_rent_paying { + // Cannot remain RentPaying if credited + post_lamports <= pre_lamports + } else { + true + } + } } - } else { - false // Only RentPaying can continue to be RentPaying } - } else { - true // Post not-RentPaying always ok } } } pub(crate) fn submit_rent_state_metrics(pre_rent_state: &RentState, post_rent_state: &RentState) { match (pre_rent_state, post_rent_state) { - (&RentState::Uninitialized, &RentState::RentPaying(_)) => { + (&RentState::Uninitialized, &RentState::RentPaying { .. }) => { inc_new_counter_info!("rent_paying_err-new_account", 1); } - (&RentState::RentPaying(_), &RentState::RentPaying(_)) => { + (&RentState::RentPaying { .. }, &RentState::RentPaying { .. }) => { inc_new_counter_info!("rent_paying_ok-legacy", 1); } - (_, &RentState::RentPaying(_)) => { + (_, &RentState::RentPaying { .. }) => { inc_new_counter_info!("rent_paying_err-other", 1); } _ => {} @@ -74,6 +92,7 @@ pub(crate) fn check_rent_state( index: usize, do_support_realloc: bool, include_account_index_in_err: bool, + prevent_crediting_accounts_that_end_rent_paying: bool, ) -> Result<()> { if let Some((pre_rent_state, post_rent_state)) = pre_rent_state.zip(post_rent_state) { let expect_msg = "account must exist at TransactionContext index if rent-states are Some"; @@ -89,6 +108,7 @@ pub(crate) fn check_rent_state( .borrow(), do_support_realloc, include_account_index_in_err.then(|| index), + prevent_crediting_accounts_that_end_rent_paying, )?; } Ok(()) @@ -101,10 +121,15 @@ pub(crate) fn check_rent_state_with_account( account_state: &AccountSharedData, do_support_realloc: bool, account_index: Option, + prevent_crediting_accounts_that_end_rent_paying: bool, ) -> Result<()> { submit_rent_state_metrics(pre_rent_state, post_rent_state); if !solana_sdk::incinerator::check_id(address) - && !post_rent_state.transition_allowed_from(pre_rent_state, do_support_realloc) + && !post_rent_state.transition_allowed_from( + pre_rent_state, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying, + ) { debug!( "Account {} not rent exempt, state {:?}", @@ -163,7 +188,10 @@ mod tests { ); assert_eq!( RentState::from_account(&rent_paying_account, &rent), - RentState::RentPaying(account_data_size) + RentState::RentPaying { + data_size: account_data_size, + lamports: rent_paying_account.lamports(), + } ); assert_eq!( RentState::from_account(&rent_exempt_account, &rent), @@ -173,21 +201,117 @@ mod tests { #[test] fn test_transition_allowed_from() { + check_transition_allowed_from( + /*prevent_crediting_accounts_that_end_rent_paying:*/ false, + ); + check_transition_allowed_from( + /*prevent_crediting_accounts_that_end_rent_paying:*/ true, + ); + } + + fn check_transition_allowed_from(prevent_crediting_accounts_that_end_rent_paying: bool) { + let do_support_realloc = true; let post_rent_state = RentState::Uninitialized; - assert!(post_rent_state.transition_allowed_from(&RentState::Uninitialized, true)); - assert!(post_rent_state.transition_allowed_from(&RentState::RentExempt, true)); - assert!(post_rent_state.transition_allowed_from(&RentState::RentPaying(0), true)); + assert!(post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentExempt, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); let post_rent_state = RentState::RentExempt; - assert!(post_rent_state.transition_allowed_from(&RentState::Uninitialized, true)); - assert!(post_rent_state.transition_allowed_from(&RentState::RentExempt, true)); - assert!(post_rent_state.transition_allowed_from(&RentState::RentPaying(0), true)); - - let post_rent_state = RentState::RentPaying(2); - assert!(!post_rent_state.transition_allowed_from(&RentState::Uninitialized, true)); - assert!(!post_rent_state.transition_allowed_from(&RentState::RentExempt, true)); - assert!(!post_rent_state.transition_allowed_from(&RentState::RentPaying(3), true)); - assert!(!post_rent_state.transition_allowed_from(&RentState::RentPaying(1), true)); - assert!(post_rent_state.transition_allowed_from(&RentState::RentPaying(2), true)); + assert!(post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentExempt, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + assert!(!post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentExempt, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 3, + lamports: 5 + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 1, + lamports: 5 + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + // Transition is always allowed if there is no account data resize or + // change in account's lamports. + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 5 + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + // Transition is always allowed if there is no account data resize and + // account's lamports is reduced. + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 7 + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + )); + // Once the feature is activated, transition is not allowed if the + // account is credited with more lamports and remains rent-paying. + assert_eq!( + post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 3 + }, + do_support_realloc, + prevent_crediting_accounts_that_end_rent_paying + ), + !prevent_crediting_accounts_that_end_rent_paying + ); } } diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index f7d45a4ab0b324..01a80d2325b486 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -436,6 +436,8 @@ impl Accounts { feature_set .is_active(&feature_set::include_account_index_in_rent_error::ID) .then(|| payer_index), + feature_set + .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::id()), ); // Feature gate only wraps the actual error return so that the metrics and debug // logging generated by `check_rent_state_with_account()` can be examined before diff --git a/runtime/src/bank/transaction_account_state_info.rs b/runtime/src/bank/transaction_account_state_info.rs index eafe1b498fc3db..bf41b9213b6452 100644 --- a/runtime/src/bank/transaction_account_state_info.rs +++ b/runtime/src/bank/transaction_account_state_info.rs @@ -64,6 +64,9 @@ impl Bank { let include_account_index_in_err = self .feature_set .is_active(&feature_set::include_account_index_in_rent_error::id()); + let prevent_crediting_accounts_that_end_rent_paying = self + .feature_set + .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::id()); for (i, (pre_state_info, post_state_info)) in pre_state_infos.iter().zip(post_state_infos).enumerate() { @@ -74,6 +77,7 @@ impl Bank { i, do_support_realloc, include_account_index_in_err, + prevent_crediting_accounts_that_end_rent_paying, ) { // Feature gate only wraps the actual error return so that the metrics and debug // logging generated by `check_rent_state()` can be examined before feature diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index ef3aeb034fb00a..9ed7fade597075 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -407,6 +407,10 @@ pub mod preserve_rent_epoch_for_rent_exempt_accounts { solana_sdk::declare_id!("HH3MUYReL2BvqqA3oEcAa7txju5GY6G4nxJ51zvsEjEZ"); } +pub mod prevent_crediting_accounts_that_end_rent_paying { + solana_sdk::declare_id!("812kqX67odAp5NFwM8D2N24cku7WTm9CHUTFUXaDkWPn"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -503,6 +507,7 @@ lazy_static! { (vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"), (cap_accounts_data_size_per_block::id(), "cap the accounts data size per block #25517"), (preserve_rent_epoch_for_rent_exempt_accounts::id(), "preserve rent epoch for rent exempt accounts #26479"), + (prevent_crediting_accounts_that_end_rent_paying::id(), "prevent crediting rent paying accounts #26606"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()