From 97d40ba3da7f70fb6f0f6e905f718c13820c09a4 Mon Sep 17 00:00:00 2001 From: Jack May Date: Thu, 24 Feb 2022 17:49:33 -0800 Subject: [PATCH] Resized accounts must be rent exempt --- Cargo.lock | 1 - programs/bpf/Cargo.lock | 1 - .../bpf/rust/realloc_invoke/src/processor.rs | 2 +- programs/bpf/tests/programs.rs | 207 +++++---- runtime/Cargo.toml | 1 - runtime/src/account_rent_state.rs | 76 +++- runtime/src/accounts.rs | 1 + runtime/src/bank.rs | 430 +++++++++++++++++- .../bank/transaction_account_state_info.rs | 4 + sdk/src/account.rs | 21 + sdk/src/system_transaction.rs | 14 + 11 files changed, 636 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6213802d9..feb4033c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5571,7 +5571,6 @@ dependencies = [ "dashmap", "dir-diff", "ed25519-dalek", - "enum-iterator", "flate2", "fnv", "index_list", diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index cbb4148f06..ce5d394a17 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -3433,7 +3433,6 @@ dependencies = [ "crossbeam-channel", "dashmap", "dir-diff", - "enum-iterator", "flate2", "fnv", "index_list", diff --git a/programs/bpf/rust/realloc_invoke/src/processor.rs b/programs/bpf/rust/realloc_invoke/src/processor.rs index e867b78618..64a504439b 100644 --- a/programs/bpf/rust/realloc_invoke/src/processor.rs +++ b/programs/bpf/rust/realloc_invoke/src/processor.rs @@ -190,7 +190,7 @@ fn process_instruction( &system_instruction::create_account( accounts[0].key, accounts[1].key, - 1, + 3000000, // large enough for rent exemption pre_len as u64, program_id, ), diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 67eb7c2589..1344841b9c 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -54,6 +54,7 @@ use { loader_instruction, message::{v0::LoadedAddresses, Message, SanitizedMessage}, pubkey::Pubkey, + rent::Rent, signature::{keypair_from_seed, Keypair, Signer}, system_instruction::{self, MAX_PERMITTED_DATA_LENGTH}, system_program, @@ -2641,11 +2642,13 @@ fn test_program_bpf_ro_account_modify() { fn test_program_bpf_realloc() { solana_logger::setup(); + const START_BALANCE: u64 = 100_000_000_000; + let GenesisConfigInfo { genesis_config, mint_keypair, .. - } = create_genesis_config(50); + } = create_genesis_config(1_000_000_000_000); let mint_pubkey = mint_keypair.pubkey(); let signer = &[&mint_keypair]; @@ -2665,7 +2668,7 @@ fn test_program_bpf_realloc() { let mut bump = 0; let keypair = Keypair::new(); let pubkey = keypair.pubkey(); - let account = AccountSharedData::new(42, 5, &program_id); + let account = AccountSharedData::new(START_BALANCE, 5, &program_id); bank.store_account(&pubkey, &account); // Realloc RO account @@ -2897,11 +2900,14 @@ fn test_program_bpf_realloc() { fn test_program_bpf_realloc_invoke() { solana_logger::setup(); + const START_BALANCE: u64 = 100_000_000_000; + let GenesisConfigInfo { - genesis_config, + mut genesis_config, mint_keypair, .. - } = create_genesis_config(50); + } = create_genesis_config(1_000_000_000_000); + genesis_config.rent = Rent::default(); let mint_pubkey = mint_keypair.pubkey(); let signer = &[&mint_keypair]; @@ -2928,7 +2934,7 @@ fn test_program_bpf_realloc_invoke() { let mut bump = 0; let keypair = Keypair::new(); let pubkey = keypair.pubkey().clone(); - let account = AccountSharedData::new(42, 5, &realloc_program_id); + let account = AccountSharedData::new(START_BALANCE, 5, &realloc_program_id); bank.store_account(&pubkey, &account); let invoke_keypair = Keypair::new(); let invoke_pubkey = invoke_keypair.pubkey().clone(); @@ -2954,6 +2960,8 @@ fn test_program_bpf_realloc_invoke() { .unwrap(), TransactionError::InstructionError(0, InstructionError::ReadonlyDataModified) ); + let account = bank.get_account(&pubkey).unwrap(); + assert_eq!(account.lamports(), START_BALANCE); // Realloc account to 0 bank_client @@ -2965,6 +2973,8 @@ fn test_program_bpf_realloc_invoke() { ), ) .unwrap(); + let account = bank.get_account(&pubkey).unwrap(); + assert_eq!(account.lamports(), START_BALANCE); let data = bank_client.get_account_data(&pubkey).unwrap().unwrap(); assert_eq!(0, data.len()); @@ -3012,54 +3022,7 @@ fn test_program_bpf_realloc_invoke() { TransactionError::InstructionError(0, InstructionError::InvalidRealloc) ); - // Realloc to max length in max increase increments - for i in 0..MAX_PERMITTED_DATA_LENGTH as usize / MAX_PERMITTED_DATA_INCREASE { - bank_client - .send_and_confirm_message( - signer, - Message::new( - &[Instruction::new_with_bytes( - realloc_invoke_program_id, - &[INVOKE_REALLOC_EXTEND_MAX, 1, i as u8, (i / 255) as u8], - vec![ - AccountMeta::new(pubkey, false), - AccountMeta::new_readonly(realloc_program_id, false), - ], - )], - Some(&mint_pubkey), - ), - ) - .unwrap(); - let data = bank_client.get_account_data(&pubkey).unwrap().unwrap(); - assert_eq!((i + 1) * MAX_PERMITTED_DATA_INCREASE, data.len()); - } - for i in 0..data.len() { - assert_eq!(data[i], 1); - } - - // and one more time should fail - assert_eq!( - bank_client - .send_and_confirm_message( - signer, - Message::new( - &[Instruction::new_with_bytes( - realloc_invoke_program_id, - &[INVOKE_REALLOC_EXTEND_MAX, 2, 1, 1], - vec![ - AccountMeta::new(pubkey, false), - AccountMeta::new_readonly(realloc_program_id, false), - ], - )], - Some(&mint_pubkey), - ) - ) - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::InvalidRealloc) - ); - - // Realloc to 0 + // Realloc account to 0 bank_client .send_and_confirm_message( signer, @@ -3069,6 +3032,8 @@ fn test_program_bpf_realloc_invoke() { ), ) .unwrap(); + let account = bank.get_account(&pubkey).unwrap(); + assert_eq!(account.lamports(), START_BALANCE); let data = bank_client.get_account_data(&pubkey).unwrap().unwrap(); assert_eq!(0, data.len()); @@ -3169,7 +3134,7 @@ fn test_program_bpf_realloc_invoke() { assert_eq!(0, data.len()); // Realloc to 100 and check via CPI - let invoke_account = AccountSharedData::new(42, 5, &realloc_invoke_program_id); + let invoke_account = AccountSharedData::new(START_BALANCE, 5, &realloc_invoke_program_id); bank.store_account(&invoke_pubkey, &invoke_account); bank_client .send_and_confirm_message( @@ -3199,42 +3164,6 @@ fn test_program_bpf_realloc_invoke() { assert_eq!(data[i], 2); } - // Realloc rescursively and fill data - let invoke_keypair = Keypair::new(); - let invoke_pubkey = invoke_keypair.pubkey().clone(); - let invoke_account = AccountSharedData::new(42, 0, &realloc_invoke_program_id); - bank.store_account(&invoke_pubkey, &invoke_account); - let mut instruction_data = vec![]; - instruction_data.extend_from_slice(&[INVOKE_REALLOC_RECURSIVE, 1]); - instruction_data.extend_from_slice(&100_usize.to_le_bytes()); - bank_client - .send_and_confirm_message( - signer, - Message::new( - &[Instruction::new_with_bytes( - realloc_invoke_program_id, - &instruction_data, - vec![ - AccountMeta::new(invoke_pubkey, false), - AccountMeta::new_readonly(realloc_invoke_program_id, false), - ], - )], - Some(&mint_pubkey), - ), - ) - .unwrap(); - let data = bank_client - .get_account_data(&invoke_pubkey) - .unwrap() - .unwrap(); - assert_eq!(200, data.len()); - for i in 0..100 { - assert_eq!(data[i], 1); - } - for i in 100..200 { - assert_eq!(data[i], 2); - } - // Create account, realloc, check let new_keypair = Keypair::new(); let new_pubkey = new_keypair.pubkey().clone(); @@ -3267,7 +3196,7 @@ fn test_program_bpf_realloc_invoke() { // Invoke, dealloc, and assign let pre_len = 100; let new_len = pre_len * 2; - let mut invoke_account = AccountSharedData::new(42, pre_len, &realloc_program_id); + let mut invoke_account = AccountSharedData::new(START_BALANCE, pre_len, &realloc_program_id); invoke_account.set_data_from_slice(&vec![1; pre_len]); bank.store_account(&invoke_pubkey, &invoke_account); let mut instruction_data = vec![]; @@ -3347,6 +3276,102 @@ fn test_program_bpf_realloc_invoke() { .unwrap(), TransactionError::InstructionError(0, InstructionError::InvalidRealloc) ); + + // Realloc to 0 + bank_client + .send_and_confirm_message( + signer, + Message::new( + &[realloc(&realloc_program_id, &pubkey, 0, &mut bump)], + Some(&mint_pubkey), + ), + ) + .unwrap(); + let data = bank_client.get_account_data(&pubkey).unwrap().unwrap(); + assert_eq!(0, data.len()); + + // Realloc to max length in max increase increments + for i in 0..MAX_PERMITTED_DATA_LENGTH as usize / MAX_PERMITTED_DATA_INCREASE { + bank_client + .send_and_confirm_message( + signer, + Message::new( + &[Instruction::new_with_bytes( + realloc_invoke_program_id, + &[INVOKE_REALLOC_EXTEND_MAX, 1, i as u8, (i / 255) as u8], + vec![ + AccountMeta::new(pubkey, false), + AccountMeta::new_readonly(realloc_program_id, false), + ], + )], + Some(&mint_pubkey), + ), + ) + .unwrap(); + let data = bank_client.get_account_data(&pubkey).unwrap().unwrap(); + assert_eq!((i + 1) * MAX_PERMITTED_DATA_INCREASE, data.len()); + } + for i in 0..data.len() { + assert_eq!(data[i], 1); + } + + // and one more time should fail + assert_eq!( + bank_client + .send_and_confirm_message( + signer, + Message::new( + &[Instruction::new_with_bytes( + realloc_invoke_program_id, + &[INVOKE_REALLOC_EXTEND_MAX, 2, 1, 1], + vec![ + AccountMeta::new(pubkey, false), + AccountMeta::new_readonly(realloc_program_id, false), + ], + )], + Some(&mint_pubkey), + ) + ) + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::InvalidRealloc) + ); + + // Realloc rescursively and fill data + let invoke_keypair = Keypair::new(); + let invoke_pubkey = invoke_keypair.pubkey().clone(); + let invoke_account = AccountSharedData::new(START_BALANCE, 0, &realloc_invoke_program_id); + bank.store_account(&invoke_pubkey, &invoke_account); + let mut instruction_data = vec![]; + instruction_data.extend_from_slice(&[INVOKE_REALLOC_RECURSIVE, 1]); + instruction_data.extend_from_slice(&100_usize.to_le_bytes()); + bank_client + .send_and_confirm_message( + signer, + Message::new( + &[Instruction::new_with_bytes( + realloc_invoke_program_id, + &instruction_data, + vec![ + AccountMeta::new(invoke_pubkey, false), + AccountMeta::new_readonly(realloc_invoke_program_id, false), + ], + )], + Some(&mint_pubkey), + ), + ) + .unwrap(); + let data = bank_client + .get_account_data(&invoke_pubkey) + .unwrap() + .unwrap(); + assert_eq!(200, data.len()); + for i in 0..100 { + assert_eq!(data[i], 1); + } + for i in 100..200 { + assert_eq!(data[i], 2); + } } #[test] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index ca5ff5cb15..7fbc1ae5cd 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -20,7 +20,6 @@ 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" index_list = "0.2.7" diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs index 297ebfaa10..736b2d2edc 100644 --- a/runtime/src/account_rent_state.rs +++ b/runtime/src/account_rent_state.rs @@ -1,5 +1,4 @@ use { - enum_iterator::IntoEnumIterator, log::*, solana_sdk::{ account::{AccountSharedData, ReadableAccount}, @@ -10,11 +9,15 @@ use { }, }; -#[derive(Debug, PartialEq, IntoEnumIterator)] +#[derive(Debug, PartialEq)] pub(crate) enum RentState { - Uninitialized, // account.lamports == 0 - RentPaying, // 0 < account.lamports < rent-exempt-minimum - RentExempt, // account.lamports >= rent-exempt-minimum + /// account.lamports == 0 + Uninitialized, + /// 0 < account.lamports < rent-exempt-minimum + /// Parameter is the size of the account data + RentPaying(usize), + /// account.lamports >= rent-exempt-minimum + RentExempt, } impl RentState { @@ -22,27 +25,42 @@ impl RentState { if account.lamports() == 0 { Self::Uninitialized } else if !rent.is_exempt(account.lamports(), account.data().len()) { - Self::RentPaying + Self::RentPaying(account.data().len()) } 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) + pub(crate) fn transition_allowed_from( + &self, + pre_rent_state: &RentState, + do_support_realloc: 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 + } + } 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); } _ => {} @@ -54,6 +72,7 @@ pub(crate) fn check_rent_state( post_rent_state: Option<&RentState>, transaction_context: &TransactionContext, index: usize, + do_support_realloc: 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"; @@ -67,6 +86,7 @@ pub(crate) fn check_rent_state( .get_account_at_index(index) .expect(expect_msg) .borrow(), + do_support_realloc, )?; } Ok(()) @@ -77,10 +97,11 @@ pub(crate) fn check_rent_state_with_account( post_rent_state: &RentState, address: &Pubkey, account_state: &AccountSharedData, + do_support_realloc: 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) + && !post_rent_state.transition_allowed_from(pre_rent_state, do_support_realloc) { debug!( "Account {} not rent exempt, state {:?}", @@ -133,7 +154,7 @@ mod tests { ); assert_eq!( RentState::from_account(&rent_paying_account, &rent), - RentState::RentPaying + RentState::RentPaying(account_data_size) ); assert_eq!( RentState::from_account(&rent_exempt_account, &rent), @@ -143,16 +164,21 @@ mod tests { #[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)); - } - } - } + 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)); + + 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)); } } diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index f933b838b4..b92b0440f2 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -377,6 +377,7 @@ impl Accounts { &payer_post_rent_state, payer_address, payer_account, + feature_set.is_active(&feature_set::do_support_realloc::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.rs b/runtime/src/bank.rs index 3f1612edb3..c16e39f45d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -16285,12 +16285,22 @@ pub(crate) mod tests { 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), + Account::new_rent_epoch( + rent_exempt_minimum - 1, + account_data_size, + &mock_program_id, + INITIAL_RENT_EPOCH + 1, + ), ); 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), + Account::new_rent_epoch( + rent_exempt_minimum, + account_data_size, + &mock_program_id, + INITIAL_RENT_EPOCH + 1, + ), ); // Activate features, including require_rent_exempt_accounts activate_all_features(&mut genesis_config); @@ -16427,6 +16437,97 @@ pub(crate) mod tests { assert!(check_account_is_rent_exempt(&rent_exempt_account.pubkey())); } + #[test] + fn test_drained_created_account() { + 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_all_features(&mut genesis_config); + + let mock_program_id = Pubkey::new_unique(); + // small enough to not pay rent, thus bypassing the data clearing rent + // mechanism + let data_size_no_rent = 100; + // large enough to pay rent, will have data cleared + let data_size_rent = 10000; + let lamports_to_transfer = 100; + + // Create legacy accounts of various kinds + let created_keypair = Keypair::new(); + + 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(); + + // Create and drain a small data size account + let create_instruction = system_instruction::create_account( + &mint_keypair.pubkey(), + &created_keypair.pubkey(), + lamports_to_transfer, + data_size_no_rent, + &mock_program_id, + ); + let account_metas = vec![ + AccountMeta::new(mint_keypair.pubkey(), true), + AccountMeta::new(created_keypair.pubkey(), true), + AccountMeta::new(mint_keypair.pubkey(), false), + ]; + let transfer_from_instruction = Instruction::new_with_bincode( + mock_program_id, + &MockTransferInstruction::Transfer(lamports_to_transfer), + account_metas, + ); + let tx = Transaction::new_signed_with_payer( + &[create_instruction, transfer_from_instruction], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &created_keypair], + recent_blockhash, + ); + + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + // account data is not stored because of zero balance even though its + // data wasn't cleared + assert!(bank.get_account(&created_keypair.pubkey()).is_none()); + + // Create and drain a large data size account + let create_instruction = system_instruction::create_account( + &mint_keypair.pubkey(), + &created_keypair.pubkey(), + lamports_to_transfer, + data_size_rent, + &mock_program_id, + ); + let account_metas = vec![ + AccountMeta::new(mint_keypair.pubkey(), true), + AccountMeta::new(created_keypair.pubkey(), true), + AccountMeta::new(mint_keypair.pubkey(), false), + ]; + let transfer_from_instruction = Instruction::new_with_bincode( + mock_program_id, + &MockTransferInstruction::Transfer(lamports_to_transfer), + account_metas, + ); + let tx = Transaction::new_signed_with_payer( + &[create_instruction, transfer_from_instruction], + Some(&mint_keypair.pubkey()), + &[&mint_keypair, &created_keypair], + recent_blockhash, + ); + + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + // account data is not stored because of zero balance + assert!(bank.get_account(&created_keypair.pubkey()).is_none()); + } + #[test] fn test_rent_state_changes_sysvars() { let GenesisConfigInfo { @@ -16879,4 +16980,329 @@ pub(crate) mod tests { ] ); } + + #[derive(Serialize, Deserialize)] + enum MockReallocInstruction { + Realloc(usize, u64, Pubkey), + } + + fn mock_realloc_process_instruction( + _first_instruction_account: usize, + data: &[u8], + invoke_context: &mut InvokeContext, + ) -> result::Result<(), InstructionError> { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + if let Ok(instruction) = bincode::deserialize(data) { + match instruction { + MockReallocInstruction::Realloc(new_size, new_balance, _) => { + // Set data length + instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .set_data_length(new_size); + + // set balance + let current_balance = instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .get_lamports(); + let diff_balance = (new_balance as i64).saturating_sub(current_balance as i64); + let amount = diff_balance.abs() as u64; + if diff_balance.is_positive() { + instruction_context + .try_borrow_instruction_account(transaction_context, 0)? + .checked_sub_lamports(amount)?; + instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .set_lamports(new_balance); + } else { + instruction_context + .try_borrow_instruction_account(transaction_context, 0)? + .checked_add_lamports(amount)?; + instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .set_lamports(new_balance); + } + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + } + + fn create_mock_realloc_tx( + payer: &Keypair, + funder: &Keypair, + reallocd: &Pubkey, + new_size: usize, + new_balance: u64, + mock_program_id: Pubkey, + recent_blockhash: Hash, + ) -> Transaction { + let account_metas = vec![ + AccountMeta::new(funder.pubkey(), false), + AccountMeta::new(*reallocd, false), + ]; + let instruction = Instruction::new_with_bincode( + mock_program_id, + &MockReallocInstruction::Realloc(new_size, new_balance, Pubkey::new_unique()), + account_metas, + ); + Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ) + } + + #[test] + fn test_resize_and_rent() { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(1_000_000_000, &Pubkey::new_unique(), 42); + genesis_config.rent = Rent::default(); + activate_all_features(&mut genesis_config); + + let mut bank = Bank::new_for_tests(&genesis_config); + + let mock_program_id = Pubkey::new_unique(); + bank.add_builtin( + "mock_realloc_program", + &mock_program_id, + mock_realloc_process_instruction, + ); + let recent_blockhash = bank.last_blockhash(); + + let account_data_size_small = 1024; + let rent_exempt_minimum_small = + genesis_config.rent.minimum_balance(account_data_size_small); + let account_data_size_large = 2048; + let rent_exempt_minimum_large = + genesis_config.rent.minimum_balance(account_data_size_large); + + let funding_keypair = Keypair::new(); + bank.store_account( + &funding_keypair.pubkey(), + &AccountSharedData::new(1_000_000_000, 0, &mock_program_id), + ); + + let rent_paying_pubkey = solana_sdk::pubkey::new_rand(); + let mut rent_paying_account = AccountSharedData::new( + rent_exempt_minimum_small - 1, + account_data_size_small, + &mock_program_id, + ); + rent_paying_account.set_rent_epoch(1); + + // restore program-owned account + bank.store_account(&rent_paying_pubkey, &rent_paying_account); + + // rent paying, realloc larger, fail because not rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_large, + rent_exempt_minimum_small - 1, + mock_program_id, + recent_blockhash, + ); + assert_eq!( + bank.process_transaction(&tx).unwrap_err(), + TransactionError::InvalidRentPayingAccount, + ); + assert_eq!( + rent_exempt_minimum_small - 1, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + // rent paying, realloc larger and rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_large, + rent_exempt_minimum_large, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_large, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + // rent exempt, realloc small, fail because not rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_small, + rent_exempt_minimum_small - 1, + mock_program_id, + recent_blockhash, + ); + assert_eq!( + bank.process_transaction(&tx).unwrap_err(), + TransactionError::InvalidRentPayingAccount, + ); + assert_eq!( + rent_exempt_minimum_large, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + // rent exempt, realloc smaller and rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_small, + rent_exempt_minimum_small, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_small, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + // rent exempt, realloc large, fail because not rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_large, + rent_exempt_minimum_large - 1, + mock_program_id, + recent_blockhash, + ); + assert_eq!( + bank.process_transaction(&tx).unwrap_err(), + TransactionError::InvalidRentPayingAccount, + ); + assert_eq!( + rent_exempt_minimum_small, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + // rent exempt, realloc large and rent exempt + let tx = create_mock_realloc_tx( + &mint_keypair, + &funding_keypair, + &rent_paying_pubkey, + account_data_size_large, + rent_exempt_minimum_large, + mock_program_id, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_large, + bank.get_account(&rent_paying_pubkey).unwrap().lamports() + ); + + let created_keypair = Keypair::new(); + + // create account, not rent exempt + let tx = system_transaction::create_account( + &mint_keypair, + &created_keypair, + recent_blockhash, + rent_exempt_minimum_small - 1, + account_data_size_small as u64, + &system_program::id(), + ); + assert_eq!( + bank.process_transaction(&tx).unwrap_err(), + TransactionError::InvalidRentPayingAccount, + ); + + // create account, rent exempt + let tx = system_transaction::create_account( + &mint_keypair, + &created_keypair, + recent_blockhash, + rent_exempt_minimum_small, + account_data_size_small as u64, + &system_program::id(), + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_small, + bank.get_account(&created_keypair.pubkey()) + .unwrap() + .lamports() + ); + + let created_keypair = Keypair::new(); + // create account, no data + let tx = system_transaction::create_account( + &mint_keypair, + &created_keypair, + recent_blockhash, + rent_exempt_minimum_small - 1, + 0, + &system_program::id(), + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_small - 1, + bank.get_account(&created_keypair.pubkey()) + .unwrap() + .lamports() + ); + + // alloc but not rent exempt + let tx = system_transaction::allocate( + &mint_keypair, + &created_keypair, + recent_blockhash, + (account_data_size_small + 1) as u64, + ); + assert_eq!( + bank.process_transaction(&tx).unwrap_err(), + TransactionError::InvalidRentPayingAccount, + ); + + // bring balance of account up to rent exemption + let tx = system_transaction::transfer( + &mint_keypair, + &created_keypair.pubkey(), + 1, + recent_blockhash, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_small, + bank.get_account(&created_keypair.pubkey()) + .unwrap() + .lamports() + ); + + // allocate as rent exempt + let tx = system_transaction::allocate( + &mint_keypair, + &created_keypair, + recent_blockhash, + account_data_size_small as u64, + ); + let result = bank.process_transaction(&tx); + assert!(result.is_ok()); + assert_eq!( + rent_exempt_minimum_small, + bank.get_account(&created_keypair.pubkey()) + .unwrap() + .lamports() + ); + } } diff --git a/runtime/src/bank/transaction_account_state_info.rs b/runtime/src/bank/transaction_account_state_info.rs index a2c2442eff..bbbb653f59 100644 --- a/runtime/src/bank/transaction_account_state_info.rs +++ b/runtime/src/bank/transaction_account_state_info.rs @@ -58,6 +58,9 @@ impl Bank { let require_rent_exempt_accounts = self .feature_set .is_active(&feature_set::require_rent_exempt_accounts::id()); + let do_support_realloc = self + .feature_set + .is_active(&feature_set::do_support_realloc::id()); for (i, (pre_state_info, post_state_info)) in pre_state_infos.iter().zip(post_state_infos).enumerate() { @@ -66,6 +69,7 @@ impl Bank { post_state_info.rent_state.as_ref(), transaction_context, i, + do_support_realloc, ) { // 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/account.rs b/sdk/src/account.rs index 87322f37a4..f5b27fed56 100644 --- a/sdk/src/account.rs +++ b/sdk/src/account.rs @@ -326,6 +326,21 @@ fn shared_new(lamports: u64, space: usize, owner: &Pubkey) - ) } +fn shared_new_rent_epoch( + lamports: u64, + space: usize, + owner: &Pubkey, + rent_epoch: Epoch, +) -> T { + T::create( + lamports, + vec![0u8; space], + *owner, + bool::default(), + rent_epoch, + ) +} + fn shared_new_ref( lamports: u64, space: usize, @@ -434,6 +449,9 @@ impl Account { ) -> Result, bincode::Error> { shared_new_ref_data_with_space(lamports, state, space, owner) } + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + shared_new_rent_epoch(lamports, space, owner, rent_epoch) + } pub fn deserialize_data(&self) -> Result { shared_deserialize_data(self) } @@ -490,6 +508,9 @@ impl AccountSharedData { ) -> Result, bincode::Error> { shared_new_ref_data_with_space(lamports, state, space, owner) } + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + shared_new_rent_epoch(lamports, space, owner, rent_epoch) + } pub fn deserialize_data(&self) -> Result { shared_deserialize_data(self) } diff --git a/sdk/src/system_transaction.rs b/sdk/src/system_transaction.rs index 526dcd7dce..4de37dd566 100644 --- a/sdk/src/system_transaction.rs +++ b/sdk/src/system_transaction.rs @@ -27,6 +27,20 @@ pub fn create_account( Transaction::new(&[from_keypair, to_keypair], message, recent_blockhash) } +/// Create and sign new SystemInstruction::Allocate transaction +pub fn allocate( + payer_keypair: &Keypair, + account_keypair: &Keypair, + recent_blockhash: Hash, + space: u64, +) -> Transaction { + let payer_pubkey = payer_keypair.pubkey(); + let account_pubkey = account_keypair.pubkey(); + let instruction = system_instruction::allocate(&account_pubkey, space); + let message = Message::new(&[instruction], Some(&payer_pubkey)); + Transaction::new(&[payer_keypair, account_keypair], message, recent_blockhash) +} + /// Create and sign new system_instruction::Assign transaction pub fn assign(from_keypair: &Keypair, recent_blockhash: Hash, program_id: &Pubkey) -> Transaction { let from_pubkey = from_keypair.pubkey();