Resized accounts must be rent exempt

This commit is contained in:
Jack May
2022-02-24 17:49:33 -08:00
parent 82cb61dc36
commit 97d40ba3da
11 changed files with 636 additions and 122 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},
@ -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));
}
}

View File

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

View File

@ -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()
);
}
}

View File

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