Resized accounts must be rent exempt

This commit is contained in:
Jack May
2022-02-24 17:49:33 -08:00
parent 44109c0cd4
commit 3bee925967
10 changed files with 639 additions and 142 deletions

View File

@ -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"

View File

@ -1,5 +1,4 @@
use {
enum_iterator::IntoEnumIterator,
log::*,
solana_sdk::{
account::{AccountSharedData, ReadableAccount},
@ -9,11 +8,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 {
@ -21,27 +24,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);
}
_ => {}
@ -53,11 +71,12 @@ pub(crate) fn check_rent_state(
post_rent_state: Option<&RentState>,
address: &Pubkey,
account: &AccountSharedData,
do_support_realloc: bool,
) -> Result<()> {
if let Some((pre_rent_state, post_rent_state)) = pre_rent_state.zip(post_rent_state) {
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 {:?}", address, account);
return Err(TransactionError::InvalidRentPayingAccount);
@ -108,7 +127,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),
@ -118,16 +137,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));
}
}

View File

@ -16575,12 +16575,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);
@ -16717,6 +16727,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 {
@ -17182,4 +17283,335 @@ 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 keyed_accounts = invoke_context.get_keyed_accounts()?;
if let Ok(instruction) = bincode::deserialize(data) {
match instruction {
MockReallocInstruction::Realloc(new_size, new_balance, _) => {
// Set data length
keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?
.account
.borrow_mut()
.set_data(vec![0; new_size]);
// set balance
let current_balance =
keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?
.account
.borrow_mut()
.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() {
keyed_account_at_index(keyed_accounts, first_instruction_account)?
.account
.borrow_mut()
.checked_sub_lamports(amount)?;
keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?
.account
.borrow_mut()
.set_lamports(new_balance);
} else {
keyed_account_at_index(keyed_accounts, first_instruction_account)?
.account
.borrow_mut()
.checked_add_lamports(amount)?;
keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?
.account
.borrow_mut()
.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()
);
}
}

View File

@ -53,6 +53,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 ((pre_state_info, post_state_info), (pubkey, account_refcell)) in pre_state_infos
.iter()
.zip(post_state_infos)
@ -63,6 +66,7 @@ impl Bank {
post_state_info.rent_state.as_ref(),
pubkey,
&account_refcell.borrow(),
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