Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Prevent creation of rent-paying accounts with non-empty data #21142

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 0 additions & 51 deletions program-test/tests/warp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions programs/bpf/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions programs/bpf_loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
165 changes: 165 additions & 0 deletions runtime/src/account_rent_state.rs
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +19 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, we could make do without the enum, and boil this logic down to a bool: is RentPaying or not. I rather like how explicit and clear the variants make the logic and exceptions, but open to other opinions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum is helpful to me! I always appreciate the added type safety as well.

}

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));
}
}
}
}
}
Loading