diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 740e7890e..4d43a6bad 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -39,6 +39,7 @@ pub use solana_sdk::reward_type::RewardType; use { crate::{ account_overrides::AccountOverrides, + account_rent_state::RentState, accounts::{ AccountAddressFilter, Accounts, LoadedTransaction, PubkeyAccountSlot, TransactionLoadResult, @@ -5032,8 +5033,27 @@ impl Bank { let mut account = self .get_account_with_fixed_root(&pubkey) .unwrap_or_default(); - if account.checked_add_lamports(rent_to_be_paid).is_err() { - // overflow adding lamports + let rent = self.rent_collector().rent; + let recipient_pre_rent_state = RentState::from_account(&account, &rent); + let distribution = account.checked_add_lamports(rent_to_be_paid); + let recipient_post_rent_state = RentState::from_account(&account, &rent); + let rent_state_transition_allowed = recipient_post_rent_state + .transition_allowed_from(&recipient_pre_rent_state); + if !rent_state_transition_allowed { + warn!( + "Rent distribution of {rent_to_be_paid} to {pubkey} results in \ + invalid RentState: {recipient_post_rent_state:?}" + ); + inc_new_counter_warn!( + "rent-distribution-rent-paying", + rent_to_be_paid as usize + ); + } + if distribution.is_err() + || (self.prevent_rent_paying_rent_recipients() + && !rent_state_transition_allowed) + { + // overflow adding lamports or resulting account is not rent-exempt self.capitalization.fetch_sub(rent_to_be_paid, Relaxed); error!( "Burned {} rent lamports instead of sending to {}", @@ -7363,6 +7383,11 @@ impl Bank { .is_active(&feature_set::no_overflow_rent_distribution::id()) } + pub fn prevent_rent_paying_rent_recipients(&self) -> bool { + self.feature_set + .is_active(&feature_set::prevent_rent_paying_rent_recipients::id()) + } + pub fn versioned_tx_message_enabled(&self) -> bool { self.feature_set .is_active(&feature_set::versioned_tx_message_enabled::id()) diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 9e16c4345..1eaf1533d 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -1115,6 +1115,189 @@ fn test_distribute_rent_to_validators_overflow() { } } +#[test] +fn test_distribute_rent_to_validators_rent_paying() { + solana_logger::setup(); + + const RENT_PER_VALIDATOR: u64 = 55; + const TOTAL_RENT: u64 = RENT_PER_VALIDATOR * 4; + + let empty_validator = ValidatorVoteKeypairs::new_rand(); + let rent_paying_validator = ValidatorVoteKeypairs::new_rand(); + let becomes_rent_exempt_validator = ValidatorVoteKeypairs::new_rand(); + let rent_exempt_validator = ValidatorVoteKeypairs::new_rand(); + let keypairs = vec![ + &empty_validator, + &rent_paying_validator, + &becomes_rent_exempt_validator, + &rent_exempt_validator, + ]; + let genesis_config_info = create_genesis_config_with_vote_accounts( + sol_to_lamports(1000.), + &keypairs, + vec![sol_to_lamports(1000.); 4], + ); + let mut genesis_config = genesis_config_info.genesis_config; + genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default + + for deactivate_feature in [false, true] { + if deactivate_feature { + genesis_config + .accounts + .remove(&feature_set::prevent_rent_paying_rent_recipients::id()) + .unwrap(); + } + let bank = Bank::new_for_tests(&genesis_config); + let rent = bank.rent_collector().rent; + let rent_exempt_minimum = rent.minimum_balance(0); + + // Make one validator have an empty identity account + let mut empty_validator_account = bank + .get_account_with_fixed_root(&empty_validator.node_keypair.pubkey()) + .unwrap(); + empty_validator_account.set_lamports(0); + bank.store_account( + &empty_validator.node_keypair.pubkey(), + &empty_validator_account, + ); + + // Make one validator almost rent-exempt, less RENT_PER_VALIDATOR + let mut becomes_rent_exempt_validator_account = bank + .get_account_with_fixed_root(&becomes_rent_exempt_validator.node_keypair.pubkey()) + .unwrap(); + becomes_rent_exempt_validator_account + .set_lamports(rent_exempt_minimum - RENT_PER_VALIDATOR); + bank.store_account( + &becomes_rent_exempt_validator.node_keypair.pubkey(), + &becomes_rent_exempt_validator_account, + ); + + // Make one validator rent-exempt + let mut rent_exempt_validator_account = bank + .get_account_with_fixed_root(&rent_exempt_validator.node_keypair.pubkey()) + .unwrap(); + rent_exempt_validator_account.set_lamports(rent_exempt_minimum); + bank.store_account( + &rent_exempt_validator.node_keypair.pubkey(), + &rent_exempt_validator_account, + ); + + let get_rent_state = |bank: &Bank, address: &Pubkey| -> RentState { + let account = bank + .get_account_with_fixed_root(address) + .unwrap_or_default(); + RentState::from_account(&account, &rent) + }; + + // Assert starting RentStates + assert_eq!( + get_rent_state(&bank, &empty_validator.node_keypair.pubkey()), + RentState::Uninitialized + ); + assert_eq!( + get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()), + RentState::RentPaying { + lamports: 42, + data_size: 0, + } + ); + assert_eq!( + get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()), + RentState::RentPaying { + lamports: rent_exempt_minimum - RENT_PER_VALIDATOR, + data_size: 0, + } + ); + assert_eq!( + get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()), + RentState::RentExempt + ); + + let old_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey()); + let old_rent_paying_validator_lamports = + bank.get_balance(&rent_paying_validator.node_keypair.pubkey()); + let old_becomes_rent_exempt_validator_lamports = + bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey()); + let old_rent_exempt_validator_lamports = + bank.get_balance(&rent_exempt_validator.node_keypair.pubkey()); + + bank.distribute_rent_to_validators(&bank.vote_accounts(), TOTAL_RENT); + + let new_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey()); + let new_rent_paying_validator_lamports = + bank.get_balance(&rent_paying_validator.node_keypair.pubkey()); + let new_becomes_rent_exempt_validator_lamports = + bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey()); + let new_rent_exempt_validator_lamports = + bank.get_balance(&rent_exempt_validator.node_keypair.pubkey()); + + // Assert ending balances; rent should be withheld if test is active and ending RentState + // is RentPaying, ie. empty_validator and rent_paying_validator + assert_eq!( + if deactivate_feature { + old_empty_validator_lamports + RENT_PER_VALIDATOR + } else { + old_empty_validator_lamports + }, + new_empty_validator_lamports + ); + + assert_eq!( + if deactivate_feature { + old_rent_paying_validator_lamports + RENT_PER_VALIDATOR + } else { + old_rent_paying_validator_lamports + }, + new_rent_paying_validator_lamports + ); + + assert_eq!( + old_becomes_rent_exempt_validator_lamports + RENT_PER_VALIDATOR, + new_becomes_rent_exempt_validator_lamports + ); + + assert_eq!( + old_rent_exempt_validator_lamports + RENT_PER_VALIDATOR, + new_rent_exempt_validator_lamports + ); + + // Assert ending RentStates + assert_eq!( + if deactivate_feature { + RentState::RentPaying { + lamports: RENT_PER_VALIDATOR, + data_size: 0, + } + } else { + RentState::Uninitialized + }, + get_rent_state(&bank, &empty_validator.node_keypair.pubkey()), + ); + assert_eq!( + if deactivate_feature { + RentState::RentPaying { + lamports: old_rent_paying_validator_lamports + RENT_PER_VALIDATOR, + data_size: 0, + } + } else { + RentState::RentPaying { + lamports: old_rent_paying_validator_lamports, + data_size: 0, + } + }, + get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()), + ); + assert_eq!( + RentState::RentExempt, + get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()), + ); + assert_eq!( + RentState::RentExempt, + get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()), + ); + } +} + #[test] fn test_rent_exempt_executable_account() { let (mut genesis_config, mint_keypair) = create_genesis_config(100_000); diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index d06699653..476df977f 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -606,6 +606,10 @@ pub mod enable_request_heap_frame_ix { solana_sdk::declare_id!("Hr1nUA9b7NJ6eChS26o7Vi8gYYDDwWD3YeBfzJkTbU86"); } +pub mod prevent_rent_paying_rent_recipients { + solana_sdk::declare_id!("Fab5oP3DmsLYCiQZXdjyqT3ukFFPrsmqhXU4WU1AWVVF"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -752,6 +756,7 @@ lazy_static! { (cap_transaction_accounts_data_size::id(), "cap transaction accounts data size up to a limit #27839"), (remove_congestion_multiplier_from_fee_calculation::id(), "Remove congestion multiplier from transaction fee calculation #29881"), (enable_request_heap_frame_ix::id(), "Enable transaction to request heap frame using compute budget instruction #30076"), + (prevent_rent_paying_rent_recipients::id(), "prevent recipients of rent rewards from ending in rent-paying state #30???"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()