diff --git a/Cargo.lock b/Cargo.lock index 476ddabaffc1d4..f625faeecbf5b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5641,6 +5641,7 @@ dependencies = [ "dashmap", "dir-diff", "ed25519-dalek", + "enum-iterator", "flate2", "fnv", "itertools 0.10.1", diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index 78496aca063a66..e7e74b42ef88e2 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -182,57 +182,6 @@ async fn clock_sysvar_updated_from_warp() { ); } -#[tokio::test] -async fn rent_collected_from_warp() { - let program_id = Pubkey::new_unique(); - // Initialize and start the test network - let program_test = ProgramTest::default(); - - let mut context = program_test.start_with_context().await; - let account_size = 100; - let keypair = Keypair::new(); - let account_lamports = Rent::default().minimum_balance(account_size) - 100; // not rent exempt - let instruction = system_instruction::create_account( - &context.payer.pubkey(), - &keypair.pubkey(), - account_lamports, - account_size as u64, - &program_id, - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &keypair], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - let account = context - .banks_client - .get_account(keypair.pubkey()) - .await - .expect("account exists") - .unwrap(); - assert_eq!(account.lamports, account_lamports); - - // Warp forward and see that rent has been collected - // This test was a bit flaky with one warp, but two warps always works - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - context.warp_to_slot(slots_per_epoch).unwrap(); - context.warp_to_slot(slots_per_epoch * 2).unwrap(); - - let account = context - .banks_client - .get_account(keypair.pubkey()) - .await - .expect("account exists") - .unwrap(); - assert!(account.lamports < account_lamports); -} - #[tokio::test] async fn stake_rewards_from_warp() { // Initialize and start the test network diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index c1df5fbb9619e4..3a59132ba6ae8f 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -800,6 +800,26 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "enum-ordinalize" version = "3.1.10" @@ -3387,6 +3407,7 @@ dependencies = [ "crossbeam-channel", "dashmap", "dir-diff", + "enum-iterator", "flate2", "fnv", "itertools 0.10.1", diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index ae844d90dd33fc..f8005adfab9b5b 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -1931,7 +1931,7 @@ mod tests { Some(&mint_keypair.pubkey()), ); assert_eq!( - TransactionError::InstructionError(1, InstructionError::ExecutableAccountNotRentExempt), + TransactionError::InvalidRentPayingAccount, bank_client .send_and_confirm_message( &[&mint_keypair, &program_keypair, &upgrade_authority_keypair], @@ -1964,7 +1964,7 @@ mod tests { ); let message = Message::new(&instructions, Some(&mint_keypair.pubkey())); assert_eq!( - TransactionError::InstructionError(1, InstructionError::ExecutableAccountNotRentExempt), + TransactionError::InvalidRentPayingAccount, bank_client .send_and_confirm_message( &[&mint_keypair, &program_keypair, &upgrade_authority_keypair], diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 68557d775723bc..93af5b45f2ac53 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -19,6 +19,7 @@ bzip2 = "0.4.3" dashmap = { version = "4.0.2", features = ["rayon", "raw-api"] } crossbeam-channel = "0.5" dir-diff = "0.3.2" +enum-iterator = "0.7.0" flate2 = "1.0.22" fnv = "1.0.7" itertools = "0.10.1" diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs new file mode 100644 index 00000000000000..c70543afdd9b1f --- /dev/null +++ b/runtime/src/account_rent_state.rs @@ -0,0 +1,165 @@ +use { + crate::bank::TransactionAccountRefCell, + enum_iterator::IntoEnumIterator, + solana_sdk::{account::ReadableAccount, native_loader, rent::Rent, sysvar}, +}; + +#[derive(Debug, PartialEq, IntoEnumIterator)] +pub(crate) enum RentState { + Uninitialized, // account.lamports == 0 + NativeOrSysvar, + EmptyData, // account.data.len == 0 + RentPaying, // 0 < account.lamports < rent-exempt-minimum for non-zero data size + RentExempt, // account.lamports >= rent-exempt-minimum for non-zero data size +} + +impl RentState { + pub(crate) fn from_account(account: &TransactionAccountRefCell, rent: &Rent) -> Self { + let account = account.1.borrow(); + if account.lamports() == 0 { + Self::Uninitialized + } else if native_loader::check_id(account.owner()) || sysvar::is_sysvar_id(account.owner()) + { + Self::NativeOrSysvar + } else if account.data().is_empty() { + Self::EmptyData + } else if !rent.is_exempt(account.lamports(), account.data().len()) { + Self::RentPaying + } else { + Self::RentExempt + } + } + + pub(crate) fn transition_allowed_from(&self, pre_rent_state: &RentState) -> bool { + // Only a legacy RentPaying account may end in the RentPaying state after message processing + !(self == &Self::RentPaying && pre_rent_state != &Self::RentPaying) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_sdk::{account::AccountSharedData, pubkey::Pubkey, system_program}, + std::{cell::RefCell, rc::Rc}, + }; + + #[test] + fn test_from_account() { + let account_address = Pubkey::new_unique(); + let program_id = Pubkey::new_unique(); + let uninitialized_account = Rc::new(RefCell::new(AccountSharedData::new( + 0, + 0, + &Pubkey::default(), + ))); + let native_program_account = Rc::new(RefCell::new(AccountSharedData::new( + 1, + 42, + &native_loader::id(), + ))); + let sysvar_account = Rc::new(RefCell::new(AccountSharedData::new( + 1, + 42, + &sysvar::clock::id(), + ))); + let empty_data_system_account = Rc::new(RefCell::new(AccountSharedData::new( + 10, + 0, + &system_program::id(), + ))); + let empty_data_other_account = + Rc::new(RefCell::new(AccountSharedData::new(10, 0, &program_id))); + + let account_data_size = 100; + + let rent = Rent::free(); + let rent_exempt_account = Rc::new(RefCell::new(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 + + assert_eq!( + RentState::from_account(&(account_address, uninitialized_account.clone()), &rent), + RentState::Uninitialized + ); + assert_eq!( + RentState::from_account(&(account_address, native_program_account.clone()), &rent), + RentState::NativeOrSysvar + ); + assert_eq!( + RentState::from_account(&(account_address, sysvar_account.clone()), &rent), + RentState::NativeOrSysvar + ); + assert_eq!( + RentState::from_account(&(account_address, empty_data_system_account.clone()), &rent), + RentState::EmptyData + ); + assert_eq!( + RentState::from_account(&(account_address, empty_data_other_account.clone()), &rent), + RentState::EmptyData + ); + assert_eq!( + RentState::from_account(&(account_address, rent_exempt_account), &rent), + RentState::RentExempt + ); + + let rent = Rent::default(); + let rent_minimum_balance = rent.minimum_balance(account_data_size); + let rent_paying_account = Rc::new(RefCell::new(AccountSharedData::new( + rent_minimum_balance.saturating_sub(1), + account_data_size, + &program_id, + ))); + let rent_exempt_account = Rc::new(RefCell::new(AccountSharedData::new( + rent.minimum_balance(account_data_size), + account_data_size, + &program_id, + ))); + + assert_eq!( + RentState::from_account(&(account_address, uninitialized_account), &rent), + RentState::Uninitialized + ); + assert_eq!( + RentState::from_account(&(account_address, native_program_account), &rent), + RentState::NativeOrSysvar + ); + assert_eq!( + RentState::from_account(&(account_address, sysvar_account), &rent), + RentState::NativeOrSysvar + ); + assert_eq!( + RentState::from_account(&(account_address, empty_data_system_account), &rent), + RentState::EmptyData + ); + assert_eq!( + RentState::from_account(&(account_address, empty_data_other_account), &rent), + RentState::EmptyData + ); + assert_eq!( + RentState::from_account(&(account_address, rent_paying_account), &rent), + RentState::RentPaying + ); + assert_eq!( + RentState::from_account(&(account_address, rent_exempt_account), &rent), + RentState::RentExempt + ); + } + + #[test] + fn test_transition_allowed_from() { + for post_rent_state in RentState::into_enum_iter() { + for pre_rent_state in RentState::into_enum_iter() { + if post_rent_state == RentState::RentPaying + && pre_rent_state != RentState::RentPaying + { + assert!(!post_rent_state.transition_allowed_from(&pre_rent_state)); + } else { + assert!(post_rent_state.transition_allowed_from(&pre_rent_state)); + } + } + } + } +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index abcb31d17ad5b0..87d6330a09d90a 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -34,6 +34,7 @@ //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. use crate::{ + account_rent_state::RentState, accounts::{AccountAddressFilter, Accounts, TransactionAccounts, TransactionLoadResult}, accounts_db::{ AccountShrinkThreshold, AccountsDbConfig, ErrorCounters, SnapshotStorages, @@ -230,9 +231,10 @@ impl ExecuteTimings { } type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "5Br3PNyyX1L7XoS4jYLt5JTeMXowLSsu7v9LhokC8vnq")] +#[frozen_abi(digest = "cMyLGdRLBURFNiZCWQT7nm4TtavaF4niwjGeXsNEULq")] pub type BankSlotDelta = SlotDelta>; -type TransactionAccountRefCells = Vec<(Pubkey, Rc>)>; +pub(crate) type TransactionAccountRefCell = (Pubkey, Rc>); +type TransactionAccountRefCells = Vec; // Eager rent collection repeats in cyclic manner. // Each cycle is composed of number of tiny pubkey subranges @@ -3669,6 +3671,41 @@ impl Bank { Ok(()) } + fn get_account_rent_states(&self, accounts: &[TransactionAccountRefCell]) -> Vec { + accounts + .iter() + .map(|account_refcell| { + RentState::from_account(account_refcell, &self.rent_collector.rent) + }) + .collect() + } + + fn check_rent_state_changes( + &self, + process_result: &mut Result<()>, + pre_rent_states: &[RentState], + post_rent_states: &[RentState], + accounts: &[TransactionAccountRefCell], + ) { + match process_result { + Ok(_) | Err(TransactionError::InstructionError(_, _)) => { + for (i, (pre_rent_state, post_rent_state)) in + pre_rent_states.iter().zip(post_rent_states).enumerate() + { + if !post_rent_state.transition_allowed_from(pre_rent_state) { + debug!( + "Account {:?} not rent exempt, lamports {:?}", + accounts[i].0, + accounts[i].1.borrow().lamports() + ); + *process_result = Err(TransactionError::InvalidRentPayingAccount) + } + } + } + _ => {} + } + } + fn collect_log_messages( log_collector: Option>, ) -> Option { @@ -3866,6 +3903,8 @@ impl Bank { ) }; + let pre_rent_state = self.get_account_rent_states(&account_refcells); + if let Some(legacy_message) = tx.message().legacy_message() { process_result = MessageProcessor::process_message( &self.instruction_processor, @@ -3889,6 +3928,20 @@ impl Bank { process_result = Err(TransactionError::UnsupportedVersion); } + let post_rent_state = self.get_account_rent_states(&account_refcells); + + if self + .feature_set + .is_active(&feature_set::require_rent_exempt_accounts::id()) + { + self.check_rent_state_changes( + &mut process_result, + &pre_rent_state, + &post_rent_state, + &account_refcells, + ); + } + transaction_log_messages.push(Self::collect_log_messages(log_collector)); inner_instructions.push(Self::compile_recorded_instructions( instruction_recorders, @@ -15244,4 +15297,382 @@ pub(crate) mod tests { .unwrap(); assert_eq!(Bank::calculate_fee(&message, 1), 11); } + + #[derive(Serialize, Deserialize)] + enum MockTransferInstruction { + Transfer(u64), + } + + fn mock_transfer_process_instruction( + first_instruction_account: usize, + data: &[u8], + invoke_context: &mut dyn InvokeContext, + ) -> result::Result<(), InstructionError> { + let keyed_accounts = invoke_context.get_keyed_accounts()?; + if let Ok(instruction) = bincode::deserialize(data) { + match instruction { + MockTransferInstruction::Transfer(amount) => { + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)? + .account + .borrow_mut() + .checked_sub_lamports(amount)?; + keyed_account_at_index(keyed_accounts, first_instruction_account + 2)? + .account + .borrow_mut() + .checked_add_lamports(amount)?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + } + + fn create_mock_transfer( + payer: &Keypair, + from: &Keypair, + to: &Keypair, + amount: u64, + mock_program_id: Pubkey, + recent_blockhash: Hash, + ) -> Transaction { + let account_metas = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(from.pubkey(), true), + AccountMeta::new(to.pubkey(), true), + ]; + let transfer_instruction = Instruction::new_with_bincode( + mock_program_id, + &MockTransferInstruction::Transfer(amount), + account_metas, + ); + Transaction::new_signed_with_payer( + &[transfer_instruction], + Some(&payer.pubkey()), + &[payer, from, to], + recent_blockhash, + ) + } + + #[test] + fn test_invalid_rent_state_changes_existing_accounts() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + + let mock_program_id = Pubkey::new_unique(); + let account_data_size = 100; + let rent_exempt_minimum = genesis_config.rent.minimum_balance(account_data_size); + + // Create legacy accounts of various kinds + let empty_data_system_account = Keypair::new(); + let empty_data_starting_balance = 42; + genesis_config.accounts.insert( + empty_data_system_account.pubkey(), + Account::new(empty_data_starting_balance, 0, &system_program::id()), + ); + let empty_data_other_account = Keypair::new(); + genesis_config.accounts.insert( + empty_data_other_account.pubkey(), + Account::new(empty_data_starting_balance, 0, &mock_program_id), + ); + let rent_paying_account = Keypair::new(); + genesis_config.accounts.insert( + rent_paying_account.pubkey(), + Account::new(rent_exempt_minimum - 1, account_data_size, &mock_program_id), + ); + let rent_exempt_account = Keypair::new(); + genesis_config.accounts.insert( + rent_exempt_account.pubkey(), + Account::new(rent_exempt_minimum, account_data_size, &mock_program_id), + ); + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let mut bank = Bank::new_for_tests(&genesis_config); + bank.add_builtin( + "mock_program", + &mock_program_id, + mock_transfer_process_instruction, + ); + let recent_blockhash = bank.last_blockhash(); + + let check_account_is_rent_exempt = |pubkey: &Pubkey| -> bool { + let account = bank.get_account(pubkey).unwrap(); + Rent::default().is_exempt(account.lamports(), account.data().len()) + }; + + // EmptyData accounts can be left as Uninitialized, RentPaying, or RentExempt, in any order + let empty_account_rent_exempt_minimum = genesis_config.rent.minimum_balance(0); + let result = bank.transfer( + empty_account_rent_exempt_minimum - empty_data_starting_balance - 1, + &mint_keypair, + &empty_data_system_account.pubkey(), + ); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt( + &empty_data_system_account.pubkey() + )); + let result = bank.transfer(1, &mint_keypair, &empty_data_system_account.pubkey()); + assert!(check_account_is_rent_exempt( + &empty_data_system_account.pubkey() + )); + assert!(result.is_ok()); + let result = bank.transfer(1, &empty_data_system_account, &mint_keypair.pubkey()); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt( + &empty_data_system_account.pubkey() + )); + let result = bank.transfer( + empty_account_rent_exempt_minimum - 1, + &empty_data_system_account, + &mint_keypair.pubkey(), + ); + assert!(result.is_ok()); + assert!(bank + .get_account(&empty_data_system_account.pubkey()) + .is_none()); + + // Check EmptyData accounts owned by a custom program; can be left as Uninitialized, + // RentPaying, or RentExempt, in any order + let result = bank.transfer( + empty_account_rent_exempt_minimum - empty_data_starting_balance - 1, + &mint_keypair, + &empty_data_other_account.pubkey(), + ); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt( + &empty_data_other_account.pubkey() + )); + let result = bank.transfer(1, &mint_keypair, &empty_data_other_account.pubkey()); + assert!(check_account_is_rent_exempt( + &empty_data_other_account.pubkey() + )); + assert!(result.is_ok()); + let tx = create_mock_transfer( + &mint_keypair, // payer + &empty_data_other_account, // from + &mint_keypair, // to + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt( + &empty_data_other_account.pubkey() + )); + let tx = create_mock_transfer( + &mint_keypair, // payer + &empty_data_other_account, // from + &mint_keypair, // to + empty_account_rent_exempt_minimum - 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(bank + .get_account(&empty_data_other_account.pubkey()) + .is_none()); + + // RentPaying account can be left as Uninitialized, in other RentPaying states, or RentExempt + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_paying_account, // from + &mint_keypair, // to + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt(&rent_paying_account.pubkey())); + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_paying_account, // from + &mint_keypair, // to + rent_exempt_minimum - 2, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(bank.get_account(&rent_paying_account.pubkey()).is_none()); + + bank.store_account( + // restore program-owned account + &rent_paying_account.pubkey(), + &AccountSharedData::new(rent_exempt_minimum - 1, account_data_size, &mock_program_id), + ); + let result = bank.transfer(1, &mint_keypair, &rent_paying_account.pubkey()); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_paying_account.pubkey())); + + // RentExempt account can only remain RentExempt or be Uninitialized + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_exempt_account, // from + &mint_keypair, // to + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_err()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + let result = bank.transfer(1, &mint_keypair, &rent_exempt_account.pubkey()); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + let tx = create_mock_transfer( + &mint_keypair, // payer + &rent_exempt_account, // from + &mint_keypair, // to + rent_exempt_minimum + 1, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(bank.get_account(&rent_exempt_account.pubkey()).is_none()); + } + + #[test] + fn test_invalid_rent_state_changes_new_accounts() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + + let mock_program_id = Pubkey::new_unique(); + let account_data_size = 100; + let rent_exempt_minimum = genesis_config.rent.minimum_balance(account_data_size); + + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let mut bank = Bank::new_for_tests(&genesis_config); + bank.add_builtin( + "mock_program", + &mock_program_id, + mock_transfer_process_instruction, + ); + let recent_blockhash = bank.last_blockhash(); + + let check_account_is_rent_exempt = |pubkey: &Pubkey| -> bool { + let account = bank.get_account(pubkey).unwrap(); + Rent::default().is_exempt(account.lamports(), account.data().len()) + }; + + // Create EmptyData account + let empty_data_account = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &empty_data_account, + recent_blockhash, + 1, + 0, + &mock_program_id, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(!check_account_is_rent_exempt(&empty_data_account.pubkey())); + + // Try to create RentPaying account + let rent_paying_account = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &rent_paying_account, + recent_blockhash, + rent_exempt_minimum - 1, + account_data_size as u64, + &mock_program_id, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_err()); + assert!(bank.get_account(&rent_paying_account.pubkey()).is_none()); + + // Try to create RentPaying account + let rent_exempt_account = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &rent_exempt_account, + recent_blockhash, + rent_exempt_minimum, + account_data_size as u64, + &mock_program_id, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); + } + + #[test] + fn test_rent_state_changes_sysvars() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(sol_to_lamports(100.), &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + // Activate features, including require_rent_exempt_accounts + activate_all_features(&mut genesis_config); + + let validator_pubkey = solana_sdk::pubkey::new_rand(); + let validator_stake_lamports = sol_to_lamports(1.); + let validator_staking_keypair = Keypair::new(); + let validator_voting_keypair = Keypair::new(); + + let validator_vote_account = vote_state::create_account( + &validator_voting_keypair.pubkey(), + &validator_pubkey, + 0, + validator_stake_lamports, + ); + + let validator_stake_account = stake_state::create_account( + &validator_staking_keypair.pubkey(), + &validator_voting_keypair.pubkey(), + &validator_vote_account, + &genesis_config.rent, + validator_stake_lamports, + ); + + genesis_config.accounts.insert( + validator_pubkey, + Account::new( + genesis_config.rent.minimum_balance(0), + 0, + &system_program::id(), + ), + ); + genesis_config.accounts.insert( + validator_staking_keypair.pubkey(), + Account::from(validator_stake_account), + ); + genesis_config.accounts.insert( + validator_voting_keypair.pubkey(), + Account::from(validator_vote_account), + ); + + let bank = Bank::new_for_tests(&genesis_config); + + // Ensure transactions with sysvars succeed, even though sysvars appear RentPaying by balance + let tx = Transaction::new_signed_with_payer( + &[stake_instruction::deactivate_stake( + &validator_staking_keypair.pubkey(), + &validator_staking_keypair.pubkey(), + )], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &validator_staking_keypair], + bank.last_blockhash(), + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 65e1cfca85ec72..44650f373087cd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))] #![allow(clippy::integer_arithmetic)] +pub mod account_rent_state; pub mod accounts; pub mod accounts_background_service; pub mod accounts_cache; diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 3422bbc79972a5..a3c41e972e1e20 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -1,3 +1,4 @@ +use crate::bank::TransactionAccountRefCell; use serde::{Deserialize, Serialize}; use solana_measure::measure::Measure; use solana_program_runtime::{ @@ -7,7 +8,7 @@ use solana_program_runtime::{ log_collector::LogCollector, }; use solana_sdk::{ - account::{AccountSharedData, WritableAccount}, + account::WritableAccount, compute_budget::ComputeBudget, feature_set::{ neon_evm_compute_budget, prevent_calling_precompiles_as_programs, requestable_heap_size, @@ -47,7 +48,7 @@ impl MessageProcessor { instruction_processor: &InstructionProcessor, message: &Message, program_indices: &[Vec], - accounts: &[(Pubkey, Rc>)], + accounts: &[TransactionAccountRefCell], rent: Rent, log_collector: Option>, executors: Rc>, @@ -96,9 +97,9 @@ impl MessageProcessor { // Fixup the special instructions key if present // before the account pre-values are taken care of - for (pubkey, accont) in accounts.iter().take(message.account_keys.len()) { + for (pubkey, account) in accounts.iter().take(message.account_keys.len()) { if instructions::check_id(pubkey) { - let mut mut_account_ref = accont.borrow_mut(); + let mut mut_account_ref = account.borrow_mut(); instructions::store_current_index( mut_account_ref.data_as_mut_slice(), instruction_index as u16, @@ -156,7 +157,7 @@ mod tests { use crate::rent_collector::RentCollector; use solana_program_runtime::invoke_context::ThisComputeMeter; use solana_sdk::{ - account::ReadableAccount, + account::{AccountSharedData, ReadableAccount}, instruction::{AccountMeta, Instruction, InstructionError}, keyed_account::keyed_account_at_index, message::Message, diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index d6c264f9324757..8887e76996ae23 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -233,6 +233,10 @@ pub mod add_compute_budget_program { solana_sdk::declare_id!("4d5AKtxoh93Dwm1vHXUU3iRATuMndx1c431KgT2td52r"); } +pub mod require_rent_exempt_accounts { + solana_sdk::declare_id!("BkFDxiJQWZXGTZaJQxH7wVEHkAmwCgSEVkrvswFfRJPD"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -286,6 +290,7 @@ lazy_static! { (requestable_heap_size::id(), "Requestable heap frame size"), (disable_fee_calculator::id(), "deprecate fee calculator"), (add_compute_budget_program::id(), "Add compute_budget_program"), + (require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction/mod.rs b/sdk/src/transaction/mod.rs index 54ee2d21d150fa..0c1c3258e80f62 100644 --- a/sdk/src/transaction/mod.rs +++ b/sdk/src/transaction/mod.rs @@ -122,6 +122,12 @@ pub enum TransactionError { /// Transaction loads a writable account that cannot be written #[error("Transaction loads a writable account that cannot be written")] InvalidWritableAccount, + + /// Transaction leaves an account with a lower balance than rent-exempt minimum + #[error( + "Transaction leaves an account with data with a lower balance than rent-exempt minimum" + )] + InvalidRentPayingAccount, } pub type Result = result::Result; diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index 0278d81d7c84c8..732e000fd16e7a 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -44,6 +44,7 @@ enum TransactionErrorType { WOULD_EXCEED_MAX_BLOCK_COST_LIMIT = 17; UNSUPPORTED_VERSION = 18; INVALID_WRITABLE_ACCOUNT = 19; + INVALID_RENT_PAYING_ACCOUNT = 20; } message InstructionError { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index c35fac5ed59880..8b14a6cdebd0cf 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -553,6 +553,7 @@ impl TryFrom for TransactionError { 17 => TransactionError::WouldExceedMaxBlockCostLimit, 18 => TransactionError::UnsupportedVersion, 19 => TransactionError::InvalidWritableAccount, + 20 => TransactionError::InvalidRentPayingAccount, _ => return Err("Invalid TransactionError"), }) } @@ -620,6 +621,9 @@ impl From for tx_by_addr::TransactionError { TransactionError::InvalidWritableAccount => { tx_by_addr::TransactionErrorType::InvalidWritableAccount } + TransactionError::InvalidRentPayingAccount => { + tx_by_addr::TransactionErrorType::InvalidRentPayingAccount + } } as i32, instruction_error: match transaction_error { TransactionError::InstructionError(index, ref instruction_error) => {