diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 01c0495e9..1405414c2 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -27,6 +27,7 @@ use crate::transaction_utils::OrderedIterator; struct CreditOnlyLock { credits: AtomicU64, lock_count: Mutex, + rent_debtor: bool, } /// This structure handles synchronization for db @@ -39,7 +40,7 @@ pub struct Accounts { account_locks: Mutex>, /// Set of credit-only accounts which are currently in the pipeline, caching account balance - /// and number of locks. On commit_credits(), we do a take() on the option so that the hashmap + /// number of locks and whether or not account owes rent. On commit_credits(), we do a take() on the option so that the hashmap /// is no longer available to be written to. credit_only_account_locks: Arc>>>, } @@ -95,6 +96,7 @@ impl Accounts { fee: u64, error_counters: &mut ErrorCounters, rent_collector: &RentCollector, + w_credit_only_account_locks: &mut RwLockWriteGuard>>, ) -> Result<(TransactionAccounts, TransactionCredits, TransactionRents)> { // Copy all the accounts let message = tx.message(); @@ -107,20 +109,33 @@ impl Accounts { return Err(TransactionError::AccountLoadedTwice); } + let credit_only_account_locks = w_credit_only_account_locks.as_mut().unwrap(); + let mut credit_only_keys: HashSet = HashSet::new(); + // There is no way to predict what program will execute without an error // If a fee can pay for execution then the program will be scheduled let mut accounts: TransactionAccounts = vec![]; let mut credits: TransactionCredits = vec![]; let mut rents: TransactionRents = vec![]; - for key in message + for (i, key) in message .account_keys .iter() .filter(|key| !message.program_ids().contains(&key)) + .enumerate() { let (account, rent) = AccountsDB::load(storage, ancestors, accounts_index, key) - .and_then(|(account, _)| rent_collector.update(account)) + .and_then( + |(mut account, _)| match rent_collector.update(&mut account) { + (Some(_), rent_due) => Some((account, rent_due)), + (None, rent_due) => Some((Account::default(), rent_due)), + }, + ) .unwrap_or_default(); + if !message.is_debitable(i) { + credit_only_keys.insert(*key); + } + accounts.push(account); credits.push(0); rents.push(rent); @@ -137,6 +152,11 @@ impl Accounts { Err(TransactionError::InsufficientFundsForFee) } else { accounts[0].lamports -= fee; + for key in credit_only_keys { + credit_only_account_locks.entry(key).and_modify(|mut lock| { + lock.rent_debtor = true; + }); + } Ok((accounts, credits, rents)) } } @@ -229,6 +249,7 @@ impl Accounts { //TODO: two locks usually leads to deadlocks, should this be one structure? let accounts_index = self.accounts_db.accounts_index.read().unwrap(); let storage = self.accounts_db.storage.read().unwrap(); + let mut w_credit_only_account_locks = self.credit_only_account_locks.write().unwrap(); OrderedIterator::new(txs, txs_iteration_order) .zip(lock_results.into_iter()) .map(|etx| match etx { @@ -246,6 +267,7 @@ impl Accounts { fee, error_counters, rent_collector, + &mut w_credit_only_account_locks, )?; let loaders = Self::load_loaders( &storage, @@ -420,6 +442,7 @@ impl Accounts { CreditOnlyLock { credits: AtomicU64::new(0), lock_count: Mutex::new(1), + rent_debtor: false, }, ); } @@ -550,49 +573,97 @@ impl Accounts { // so will fail the lock // 2) Any transaction that grabs a lock and then commit_credits clears the HashMap will find // the HashMap is None on unlock_accounts, and will perform a no-op. - pub fn commit_credits(&self, ancestors: &HashMap, fork: Fork) { + pub fn commit_credits_and_rents( + &self, + rent_collector: &RentCollector, + ancestors: &HashMap, + fork: Fork, + ) -> u64 { // Clear the credit only hashmap so that no further transactions can modify it let credit_only_account_locks = Self::take_credit_only(&self.credit_only_account_locks) - .expect("Credit only locks didn't exist in commit_credits"); - self.store_credit_only_credits(credit_only_account_locks, ancestors, fork); + .expect("Credit only locks didn't exist in commit_credits_and_rents"); + self.store_credit_only_credits_and_rents( + credit_only_account_locks, + rent_collector, + ancestors, + fork, + ) } /// Used only for tests to store credit-only accounts after every transaction - pub fn commit_credits_unsafe(&self, ancestors: &HashMap, fork: Fork) { + pub fn commit_credits_and_rents_unsafe( + &self, + rent_collector: &RentCollector, + ancestors: &HashMap, + fork: Fork, + ) -> u64 { // Clear the credit only hashmap so that no further transactions can modify it let mut w_credit_only_account_locks = self.credit_only_account_locks.write().unwrap(); let w_credit_only_account_locks = Self::get_write_access_credit_only(&mut w_credit_only_account_locks) .expect("Credit only locks didn't exist in commit_credits"); - self.store_credit_only_credits(w_credit_only_account_locks.drain(), ancestors, fork); + self.store_credit_only_credits_and_rents( + w_credit_only_account_locks.drain(), + rent_collector, + ancestors, + fork, + ) } - fn store_credit_only_credits( + fn store_credit_only_credits_and_rents( &self, credit_only_account_locks: I, + rent_collector: &RentCollector, ancestors: &HashMap, fork: Fork, - ) where + ) -> u64 + where I: IntoIterator, { - for (pubkey, lock) in credit_only_account_locks { - let lock_count = *lock.lock_count.lock().unwrap(); - if lock_count != 0 { - warn!( - "dropping credit-only lock on {}, still has {} locks", - pubkey, lock_count - ); - } - let credit = lock.credits.load(Ordering::Relaxed); - if credit > 0 { - let mut account = self - .load_slow(ancestors, &pubkey) - .map(|(account, _)| account) - .unwrap_or_default(); + let mut accounts: HashMap = HashMap::new(); + let mut total_rent_collected = 0; + + { + let accounts_index = self.accounts_db.accounts_index.read().unwrap(); + let storage = self.accounts_db.storage.read().unwrap(); + + for (pubkey, lock) in credit_only_account_locks { + let lock_count = *lock.lock_count.lock().unwrap(); + if lock_count != 0 { + warn!( + "dropping credit-only lock on {}, still has {} locks", + pubkey, lock_count + ); + } + let credit = lock.credits.load(Ordering::Relaxed); + + let (mut account, _) = + AccountsDB::load(&storage, ancestors, &accounts_index, &pubkey) + .unwrap_or_default(); + + if lock.rent_debtor { + let (rent_debtor_account, rent_collected) = + match rent_collector.update(&mut account) { + (Some(_), rent_due) => (account, rent_due), + (None, rent_due) => (Account::default(), rent_due), + }; + total_rent_collected += rent_collected; + account = rent_debtor_account; + } + account.lamports += credit; - self.store_slow(fork, &pubkey, &account); + accounts.insert(pubkey, account); } } + + let account_to_store: Vec<(&Pubkey, &Account)> = accounts + .iter() + .map(|(key, account)| (key, account)) + .collect(); + + self.accounts_db.store(fork, &account_to_store); + + total_rent_collected } fn collect_accounts_to_store<'a>( @@ -664,6 +735,7 @@ mod tests { use solana_sdk::fee_calculator::FeeCalculator; use solana_sdk::hash::Hash; use solana_sdk::instruction::CompiledInstruction; + use solana_sdk::rent_calculator::RentCalculator; use solana_sdk::signature::{Keypair, KeypairUtil}; use solana_sdk::sysvar; use solana_sdk::transaction::Transaction; @@ -672,10 +744,11 @@ mod tests { use std::{thread, time}; use tempfile::TempDir; - fn load_accounts_with_fee( + fn load_accounts_with_fee_and_rent( tx: Transaction, ka: &Vec<(Pubkey, Account)>, fee_calculator: &FeeCalculator, + rent_collector: &RentCollector, error_counters: &mut ErrorCounters, ) -> Vec> { let mut hash_queue = BlockhashQueue::new(100); @@ -686,7 +759,6 @@ mod tests { } let ancestors = vec![(0, 0)].into_iter().collect(); - let rent_collector = RentCollector::default(); let res = accounts.load_accounts( &ancestors, &[tx], @@ -705,7 +777,8 @@ mod tests { error_counters: &mut ErrorCounters, ) -> Vec> { let fee_calculator = FeeCalculator::default(); - load_accounts_with_fee(tx, ka, &fee_calculator, error_counters) + let rent_collector: RentCollector = RentCollector::default(); + load_accounts_with_fee_and_rent(tx, ka, &fee_calculator, &rent_collector, error_counters) } #[test] @@ -809,8 +882,15 @@ mod tests { let fee_calculator = FeeCalculator::new(10, 0); assert_eq!(fee_calculator.calculate_fee(tx.message()), 10); - let loaded_accounts = - load_accounts_with_fee(tx, &accounts, &fee_calculator, &mut error_counters); + let rent_collector = RentCollector::default(); + + let loaded_accounts = load_accounts_with_fee_and_rent( + tx, + &accounts, + &fee_calculator, + &rent_collector, + &mut error_counters, + ); assert_eq!(error_counters.insufficient_funds, 1); assert_eq!(loaded_accounts.len(), 1); @@ -1414,17 +1494,29 @@ mod tests { } #[test] - fn test_commit_credits() { + fn test_commit_credits_and_rents() { let pubkey0 = Pubkey::new_rand(); let pubkey1 = Pubkey::new_rand(); let pubkey2 = Pubkey::new_rand(); + let overdue_account_pubkey = Pubkey::new_rand(); - let account0 = Account::new(1, 0, &Pubkey::default()); - let account1 = Account::new(2, 0, &Pubkey::default()); + let account0 = Account::new(1, 1, &Pubkey::default()); + let account1 = Account::new(2, 1, &Pubkey::default()); + let overdue_account = Account::new(1, 2, &Pubkey::default()); let accounts = Accounts::new(None); accounts.store_slow(0, &pubkey0, &account0); accounts.store_slow(0, &pubkey1, &account1); + accounts.store_slow(0, &overdue_account_pubkey, &overdue_account); + + let mut rent_collector = RentCollector::default(); + rent_collector.epoch = 2; + rent_collector.slots_per_year = 400_f64; + rent_collector.rent_calculator = RentCalculator { + burn_percent: 10, + exemption_threshold: 8.0, + lamports_per_byte_year: 1, + }; { let mut credit_only_account_locks = accounts.credit_only_account_locks.write().unwrap(); @@ -1432,8 +1524,9 @@ mod tests { credit_only_account_locks.insert( pubkey0, CreditOnlyLock { - credits: AtomicU64::new(0), + credits: AtomicU64::new(1), lock_count: Mutex::new(1), + rent_debtor: true, }, ); credit_only_account_locks.insert( @@ -1441,6 +1534,7 @@ mod tests { CreditOnlyLock { credits: AtomicU64::new(5), lock_count: Mutex::new(1), + rent_debtor: true, }, ); credit_only_account_locks.insert( @@ -1448,24 +1542,74 @@ mod tests { CreditOnlyLock { credits: AtomicU64::new(10), lock_count: Mutex::new(1), + rent_debtor: false, + }, + ); + credit_only_account_locks.insert( + overdue_account_pubkey, + CreditOnlyLock { + credits: AtomicU64::new(3), + lock_count: Mutex::new(1), + rent_debtor: true, }, ); } let ancestors = vec![(0, 0)].into_iter().collect(); - accounts.commit_credits_unsafe(&ancestors, 0); - // No change when CreditOnlyLock credits are 0 + // This account has data length of 2 + assert_eq!( + accounts + .load_slow(&ancestors, &overdue_account_pubkey) + .unwrap() + .0 + .data + .len(), + 2 + ); + + // Total rent collected should be: rent from pubkey0(1) + rent from pubkey1(1) + assert_eq!( + accounts.commit_credits_and_rents_unsafe(&rent_collector, &ancestors, 0), + 3 + ); + + // New balance should be previous balance plus CreditOnlyLock credits - rent + // Balance(1) - Rent(1) + Credit(1) assert_eq!( accounts.load_slow(&ancestors, &pubkey0).unwrap().0.lamports, 1 ); - // New balance should equal previous balance plus CreditOnlyLock credits + + // New balance should equal previous balance plus CreditOnlyLock credits - rent + // Balance(2) - Rent(1) + Credit(5) assert_eq!( accounts.load_slow(&ancestors, &pubkey1).unwrap().0.lamports, - 7 + 6 ); - // New account should be created + + // Rent overdue account, will be reseted and it's lamport will be consumed + // Balance(1) - Rent(1)(Due to insufficient balance) + Credit(3) + assert_eq!( + accounts + .load_slow(&ancestors, &overdue_account_pubkey) + .unwrap() + .0 + .lamports, + 3 + ); + // The reseted account should have data length of zero + assert_eq!( + accounts + .load_slow(&ancestors, &overdue_account_pubkey) + .unwrap() + .0 + .data + .len(), + 0 + ); + + // New account should be created and no rent should be charged assert_eq!( accounts.load_slow(&ancestors, &pubkey2).unwrap().0.lamports, 10 @@ -1551,6 +1695,7 @@ mod tests { CreditOnlyLock { credits: AtomicU64::new(0), lock_count: Mutex::new(1), + rent_debtor: false, }, ); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 402fe36dd..c23d9c155 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -228,6 +228,11 @@ pub struct Bank { /// latest rent collector, knows the epoch rent_collector: RentCollector, + /// tallied credit-debit rent for this slot + #[serde(serialize_with = "serialize_atomicu64")] + #[serde(deserialize_with = "deserialize_atomicu64")] + tallied_credit_debit_rent: AtomicU64, + /// initialized from genesis epoch_schedule: EpochSchedule, @@ -333,6 +338,7 @@ impl Bank { parent_hash: parent.hash(), collector_id: *collector_id, collector_fees: AtomicU64::new(0), + tallied_credit_debit_rent: AtomicU64::new(0), ancestors: HashMap::new(), hash: RwLock::new(Hash::default()), is_delta: AtomicBool::new(false), @@ -557,9 +563,11 @@ impl Bank { if *hash == Hash::default() { // finish up any deferred changes to account state - self.commit_credits(); self.collect_fees(); + let collected_rent = self.commit_credits_and_rents(); + self.distribute_rent(collected_rent); + // freeze is a one-way trip, idempotent *hash = self.hash_internal_state(); true @@ -578,6 +586,10 @@ impl Bank { &self.epoch_schedule } + pub fn get_tallied_credit_debit_rent(&self) -> u64 { + self.tallied_credit_debit_rent.load(Ordering::Relaxed) + } + /// squash the parent's state up into this Bank, /// this Bank becomes a root pub fn squash(&self) { @@ -824,9 +836,11 @@ impl Bank { self.process_transactions(&txs)[0].clone()?; // Call this instead of commit_credits(), so that the credit-only locks hashmap on this // bank isn't deleted - self.rc - .accounts - .commit_credits_unsafe(&self.ancestors, self.slot()); + self.rc.accounts.commit_credits_and_rents_unsafe( + &self.rent_collector, + &self.ancestors, + self.slot(), + ); tx.signatures .get(0) .map_or(Ok(()), |sig| self.get_signature_status(sig).unwrap()) @@ -1198,6 +1212,7 @@ impl Bank { write_time.stop(); debug!("store: {}us txs_len={}", write_time.as_us(), txs.len(),); self.update_transaction_statuses(txs, iteration_order, &executed); + self.tally_credit_debit_rent(txs, iteration_order, &executed, loaded_accounts); self.filter_program_errors_and_collect_fee(txs, iteration_order, executed) } @@ -1566,10 +1581,60 @@ impl Bank { ); } - fn commit_credits(&self) { - self.rc - .accounts - .commit_credits(&self.ancestors, self.slot()); + fn commit_credits_and_rents(&self) -> u64 { + self.rc.accounts.commit_credits_and_rents( + &self.rent_collector, + &self.ancestors, + self.slot(), + ) + } + + fn tally_credit_debit_rent( + &self, + txs: &[Transaction], + iteration_order: Option<&[usize]>, + res: &[Result<()>], + loaded_accounts: &[Result], + ) { + let mut collected_rent = 0; + for (i, (raccs, tx)) in loaded_accounts + .iter() + .zip(OrderedIterator::new(txs, iteration_order)) + .enumerate() + { + if res[i].is_err() || raccs.is_err() { + continue; + } + + let message = &tx.message(); + let acc = raccs.as_ref().unwrap(); + + for (_i, rent) in acc + .3 + .iter() + .enumerate() + .filter(|(i, _rent)| message.is_debitable(*i)) + { + collected_rent += rent; + } + } + + self.tallied_credit_debit_rent + .fetch_add(collected_rent, Ordering::Relaxed); + } + + fn distribute_rent(&self, credit_only_collected_rent: u64) { + let total_rent_collected = + credit_only_collected_rent + self.tallied_credit_debit_rent.load(Ordering::Relaxed); + + if total_rent_collected != 0 { + let burned_portion = (total_rent_collected + * u64::from(self.rent_collector.rent_calculator.burn_percent)) + / 100; + let _rent_to_be_distributed = total_rent_collected - burned_portion; + // TODO: distribute remaining rent amount to validators + // self.capitalization.fetch_sub(burned_portion, Ordering::Relaxed); + } } } @@ -1592,6 +1657,7 @@ mod tests { status_cache::MAX_CACHE_ENTRIES, }; use bincode::{deserialize_from, serialize_into, serialized_size}; + use solana_sdk::system_program::solana_system_program; use solana_sdk::{ clock::DEFAULT_TICKS_PER_SLOT, epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, @@ -1753,6 +1819,349 @@ mod tests { assert_eq!(bank.transaction_count(), 2); } + #[test] + fn test_credit_debit_rent_no_side_effect_on_hash() { + let (mut genesis_block, _mint_keypair) = create_genesis_block(10); + let credit_only_key1 = Pubkey::new_rand(); + let credit_only_key2 = Pubkey::new_rand(); + let credit_debit_keypair1: Keypair = Keypair::new(); + let credit_debit_keypair2: Keypair = Keypair::new(); + + let rent_overdue_credit_only_key1 = Pubkey::new_rand(); + let rent_overdue_credit_only_key2 = Pubkey::new_rand(); + let rent_overdue_credit_debit_keypair1 = Keypair::new(); + let rent_overdue_credit_debit_keypair2 = Keypair::new(); + + genesis_block.rent_calculator = RentCalculator { + lamports_per_byte_year: 1, + exemption_threshold: 21.0, + burn_percent: 10, + }; + + let root_bank = Arc::new(Bank::new(&genesis_block)); + let bank = Bank::new_from_parent( + &root_bank, + &Pubkey::default(), + 2 * (SECONDS_PER_YEAR + // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s + *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) + // / ticks/slot + / genesis_block.ticks_per_slot as f64) as u64, + ); + + let root_bank_2 = Arc::new(Bank::new(&genesis_block)); + let bank_with_success_txs = Bank::new_from_parent( + &root_bank_2, + &Pubkey::default(), + 2 * (SECONDS_PER_YEAR + // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s + *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) + // / ticks/slot + / genesis_block.ticks_per_slot as f64) as u64, + ); + assert_eq!(bank.last_blockhash(), genesis_block.hash()); + + // Initialize credit-debit and credit only accounts + let credit_debit_account1 = Account::new(20, 1, &Pubkey::default()); + let credit_debit_account2 = Account::new(20, 1, &Pubkey::default()); + let credit_only_account1 = Account::new(3, 1, &Pubkey::default()); + let credit_only_account2 = Account::new(3, 1, &Pubkey::default()); + + bank.store_account(&credit_debit_keypair1.pubkey(), &credit_debit_account1); + bank.store_account(&credit_debit_keypair2.pubkey(), &credit_debit_account2); + bank.store_account(&credit_only_key1, &credit_only_account1); + bank.store_account(&credit_only_key2, &credit_only_account2); + + bank_with_success_txs + .store_account(&credit_debit_keypair1.pubkey(), &credit_debit_account1); + bank_with_success_txs + .store_account(&credit_debit_keypair2.pubkey(), &credit_debit_account2); + bank_with_success_txs.store_account(&credit_only_key1, &credit_only_account1); + bank_with_success_txs.store_account(&credit_only_key2, &credit_only_account2); + + let rent_overdue_credit_debit_account1 = Account::new(2, 1, &Pubkey::default()); + let rent_overdue_credit_debit_account2 = Account::new(2, 1, &Pubkey::default()); + let rent_overdue_credit_only_account1 = Account::new(1, 1, &Pubkey::default()); + let rent_overdue_credit_only_account2 = Account::new(1, 1, &Pubkey::default()); + + bank.store_account( + &rent_overdue_credit_debit_keypair1.pubkey(), + &rent_overdue_credit_debit_account1, + ); + bank.store_account( + &rent_overdue_credit_debit_keypair2.pubkey(), + &rent_overdue_credit_debit_account2, + ); + bank.store_account( + &rent_overdue_credit_only_key1, + &rent_overdue_credit_only_account1, + ); + bank.store_account( + &rent_overdue_credit_only_key2, + &rent_overdue_credit_only_account2, + ); + + bank_with_success_txs.store_account( + &rent_overdue_credit_debit_keypair1.pubkey(), + &rent_overdue_credit_debit_account1, + ); + bank_with_success_txs.store_account( + &rent_overdue_credit_debit_keypair2.pubkey(), + &rent_overdue_credit_debit_account2, + ); + bank_with_success_txs.store_account( + &rent_overdue_credit_only_key1, + &rent_overdue_credit_only_account1, + ); + bank_with_success_txs.store_account( + &rent_overdue_credit_only_key2, + &rent_overdue_credit_only_account2, + ); + + // Make native instruction loader rent exempt + let system_program_id = solana_system_program().1; + let mut system_program_account = bank.get_account(&system_program_id).unwrap(); + system_program_account.lamports = + bank.get_minimum_balance_for_rent_exemption(system_program_account.data.len()); + bank.store_account(&system_program_id, &system_program_account); + bank_with_success_txs.store_account(&system_program_id, &system_program_account); + + let t1 = system_transaction::transfer( + &credit_debit_keypair1, + &rent_overdue_credit_only_key1, + 1, + genesis_block.hash(), + ); + let t2 = system_transaction::transfer( + &rent_overdue_credit_debit_keypair1, + &credit_only_key1, + 1, + genesis_block.hash(), + ); + let t3 = system_transaction::transfer( + &credit_debit_keypair2, + &credit_only_key2, + 1, + genesis_block.hash(), + ); + let t4 = system_transaction::transfer( + &rent_overdue_credit_debit_keypair2, + &rent_overdue_credit_only_key2, + 1, + genesis_block.hash(), + ); + let res = bank.process_transactions(&vec![t1.clone(), t2.clone(), t3.clone(), t4.clone()]); + + assert_eq!(res.len(), 4); + assert_eq!(res[0], Ok(())); + assert_eq!(res[1], Err(TransactionError::AccountNotFound)); + assert_eq!(res[2], Ok(())); + assert_eq!(res[3], Err(TransactionError::AccountNotFound)); + + bank.freeze(); + + let rwlockguard_bank_hash = bank.hash.read().unwrap(); + let bank_hash = rwlockguard_bank_hash.as_ref(); + + let res = bank_with_success_txs.process_transactions(&vec![t3.clone(), t1.clone()]); + + assert_eq!(res.len(), 2); + assert_eq!(res[0], Ok(())); + assert_eq!(res[1], Ok(())); + + bank_with_success_txs.freeze(); + + let rwlockguard_bank_with_success_txs_hash = bank_with_success_txs.hash.read().unwrap(); + let bank_with_success_txs_hash = rwlockguard_bank_with_success_txs_hash.as_ref(); + + assert_eq!(bank_with_success_txs_hash, bank_hash); + } + + #[test] + fn test_credit_debit_rent() { + let (mut genesis_block, _mint_keypair) = create_genesis_block(10); + let credit_only_key1 = Pubkey::new_rand(); + let credit_only_key2 = Pubkey::new_rand(); + let credit_debit_keypair1: Keypair = Keypair::new(); + let credit_debit_keypair2: Keypair = Keypair::new(); + + let rent_overdue_credit_only_key1 = Pubkey::new_rand(); + let rent_overdue_credit_only_key2 = Pubkey::new_rand(); + let rent_overdue_credit_debit_keypair1 = Keypair::new(); + let rent_overdue_credit_debit_keypair2 = Keypair::new(); + + genesis_block.rent_calculator = RentCalculator { + lamports_per_byte_year: 1, + exemption_threshold: 21.0, + burn_percent: 10, + }; + + let root_bank = Bank::new(&genesis_block); + let bank = Bank::new_from_parent( + &Arc::new(root_bank), + &Pubkey::default(), + 2 * (SECONDS_PER_YEAR + // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s + *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) + // / ticks/slot + / genesis_block.ticks_per_slot as f64) as u64, + ); + assert_eq!(bank.last_blockhash(), genesis_block.hash()); + + // Initialize credit-debit and credit only accounts + let credit_debit_account1 = Account::new(20, 1, &Pubkey::default()); + let credit_debit_account2 = Account::new(20, 1, &Pubkey::default()); + let credit_only_account1 = Account::new(3, 1, &Pubkey::default()); + let credit_only_account2 = Account::new(3, 1, &Pubkey::default()); + + bank.store_account(&credit_debit_keypair1.pubkey(), &credit_debit_account1); + bank.store_account(&credit_debit_keypair2.pubkey(), &credit_debit_account2); + bank.store_account(&credit_only_key1, &credit_only_account1); + bank.store_account(&credit_only_key2, &credit_only_account2); + + let rent_overdue_credit_debit_account1 = Account::new(2, 1, &Pubkey::default()); + let rent_overdue_credit_debit_account2 = Account::new(2, 1, &Pubkey::default()); + let rent_overdue_credit_only_account1 = Account::new(1, 1, &Pubkey::default()); + let rent_overdue_credit_only_account2 = Account::new(1, 1, &Pubkey::default()); + + bank.store_account( + &rent_overdue_credit_debit_keypair1.pubkey(), + &rent_overdue_credit_debit_account1, + ); + bank.store_account( + &rent_overdue_credit_debit_keypair2.pubkey(), + &rent_overdue_credit_debit_account2, + ); + bank.store_account( + &rent_overdue_credit_only_key1, + &rent_overdue_credit_only_account1, + ); + bank.store_account( + &rent_overdue_credit_only_key2, + &rent_overdue_credit_only_account2, + ); + + // Make native instruction loader rent exempt + let system_program_id = solana_system_program().1; + let mut system_program_account = bank.get_account(&system_program_id).unwrap(); + system_program_account.lamports = + bank.get_minimum_balance_for_rent_exemption(system_program_account.data.len()); + bank.store_account(&system_program_id, &system_program_account); + + let total_lamports_before_txs = system_program_account.lamports + + rent_overdue_credit_debit_account1.lamports + + rent_overdue_credit_debit_account2.lamports + + rent_overdue_credit_only_account1.lamports + + rent_overdue_credit_only_account2.lamports + + credit_debit_account1.lamports + + credit_debit_account2.lamports + + credit_only_account1.lamports + + credit_only_account2.lamports; + + let t1 = system_transaction::transfer( + &credit_debit_keypair1, + &rent_overdue_credit_only_key1, + 1, + genesis_block.hash(), + ); + let t2 = system_transaction::transfer( + &rent_overdue_credit_debit_keypair1, + &credit_only_key1, + 1, + genesis_block.hash(), + ); + let t3 = system_transaction::transfer( + &credit_debit_keypair2, + &credit_only_key2, + 1, + genesis_block.hash(), + ); + let t4 = system_transaction::transfer( + &rent_overdue_credit_debit_keypair2, + &rent_overdue_credit_only_key2, + 1, + genesis_block.hash(), + ); + let res = bank.process_transactions(&vec![t1.clone(), t2.clone(), t3.clone(), t4.clone()]); + + let mut total_lamports_after_txs = 0; + + assert_eq!(res.len(), 4); + assert_eq!(res[0], Ok(())); + assert_eq!(res[1], Err(TransactionError::AccountNotFound)); + assert_eq!(res[2], Ok(())); + assert_eq!(res[3], Err(TransactionError::AccountNotFound)); + + // We haven't yet made any changes to credit only accounts + assert_eq!(bank.get_balance(&credit_only_key1), 3); + assert_eq!(bank.get_balance(&credit_only_key2), 3); + assert_eq!(bank.get_balance(&rent_overdue_credit_only_key1), 1); + assert_eq!(bank.get_balance(&rent_overdue_credit_only_key2), 1); + + assert_eq!( + bank.get_balance(&system_program_id), + system_program_account.lamports + ); + total_lamports_after_txs += bank.get_balance(&system_program_id); + + // Credit-debit account's rent is already deducted + // 20 - 1(Transferred) - 2(Rent) + assert_eq!(bank.get_balance(&credit_debit_keypair1.pubkey()), 17); + total_lamports_after_txs += bank.get_balance(&credit_debit_keypair1.pubkey()); + assert_eq!(bank.get_balance(&credit_debit_keypair2.pubkey()), 17); + total_lamports_after_txs += bank.get_balance(&credit_debit_keypair2.pubkey()); + // Since this credit-debit accounts are unable to pay rent, load_tx_account failed, as they are + // the signer account. No change was done. + assert_eq!( + bank.get_balance(&rent_overdue_credit_debit_keypair1.pubkey()), + 2 + ); + total_lamports_after_txs += bank.get_balance(&rent_overdue_credit_debit_keypair1.pubkey()); + assert_eq!( + bank.get_balance(&rent_overdue_credit_debit_keypair2.pubkey()), + 2 + ); + total_lamports_after_txs += bank.get_balance(&rent_overdue_credit_debit_keypair2.pubkey()); + + // Credit-debit account's rent is stored in `tallied_credit_debit_rent` + // Rent deducted is: 2+2 + assert_eq!(bank.get_tallied_credit_debit_rent(), 4); + total_lamports_after_txs += bank.get_tallied_credit_debit_rent(); + + // Rent deducted is: 2+1 + let commited_credit_only_rent = bank.commit_credits_and_rents(); + assert_eq!(commited_credit_only_rent, 3); + total_lamports_after_txs += commited_credit_only_rent; + + // No rent deducted because tx failed + assert_eq!(bank.get_balance(&credit_only_key1), 3); + total_lamports_after_txs += bank.get_balance(&credit_only_key1); + // Now, we have credited credits and debited rent + // 3 + 1(Transferred) - 2(Rent) + assert_eq!(bank.get_balance(&credit_only_key2), 2); + total_lamports_after_txs += bank.get_balance(&credit_only_key2); + + // Since we were unable to pay rent, the account was reset, rent got deducted. + // And credit went to that overwritten account + // Rent deducted: 1 + assert_eq!(bank.get_balance(&rent_overdue_credit_only_key1), 1); + assert_eq!( + bank.get_account(&rent_overdue_credit_only_key1) + .unwrap() + .data + .len(), + 0 + ); + total_lamports_after_txs += bank.get_balance(&rent_overdue_credit_only_key1); + + // No rent got deducted as, we were unable to load accounts (load_tx_accounts errored out) + assert_eq!(bank.get_balance(&rent_overdue_credit_only_key2), 1); + total_lamports_after_txs += bank.get_balance(&rent_overdue_credit_only_key2); + + // total lamports in circulation should be same + assert_eq!(total_lamports_after_txs, total_lamports_before_txs); + } + #[test] fn test_one_source_two_tx_one_batch() { let (genesis_block, mint_keypair) = create_genesis_block(1); @@ -1764,7 +2173,7 @@ mod tests { let t1 = system_transaction::transfer(&mint_keypair, &key1, 1, genesis_block.hash()); let t2 = system_transaction::transfer(&mint_keypair, &key2, 1, genesis_block.hash()); let res = bank.process_transactions(&vec![t1.clone(), t2.clone()]); - bank.commit_credits(); + bank.commit_credits_and_rents(); assert_eq!(res.len(), 2); assert_eq!(res[0], Ok(())); @@ -2163,9 +2572,11 @@ mod tests { system_transaction::transfer(&payer1, &recipient.pubkey(), 1, genesis_block.hash()); let txs = vec![tx0, tx1, tx2]; let results = bank.process_transactions(&txs); - bank.rc - .accounts - .commit_credits_unsafe(&bank.ancestors, bank.slot()); + bank.rc.accounts.commit_credits_and_rents_unsafe( + &bank.rent_collector, + &bank.ancestors, + bank.slot(), + ); // If multiple transactions attempt to deposit into the same account, they should succeed, // since System Transfer `To` accounts are given credit-only handling @@ -2184,9 +2595,11 @@ mod tests { system_transaction::transfer(&recipient, &payer0.pubkey(), 1, genesis_block.hash()); let txs = vec![tx0, tx1]; let results = bank.process_transactions(&txs); - bank.rc - .accounts - .commit_credits_unsafe(&bank.ancestors, bank.slot()); + bank.rc.accounts.commit_credits_and_rents_unsafe( + &bank.rent_collector, + &bank.ancestors, + bank.slot(), + ); // However, an account may not be locked as credit-only and credit-debit at the same time. assert_eq!(results[0], Ok(())); assert_eq!(results[1], Err(TransactionError::AccountInUse)); diff --git a/runtime/src/rent_collector.rs b/runtime/src/rent_collector.rs index e9ef52332..53420e526 100644 --- a/runtime/src/rent_collector.rs +++ b/runtime/src/rent_collector.rs @@ -35,9 +35,9 @@ impl RentCollector { // updates this account's lamports and status and returns // the account rent collected, if any // - pub fn update(&self, mut account: Account) -> Option<(Account, u64)> { + pub fn update<'a>(&self, account: &'a mut Account) -> (Option<&'a Account>, u64) { if account.data.is_empty() || account.rent_epoch > self.epoch { - Some((account, 0)) + (Some(account), 0) } else { let slots_elapsed: u64 = (account.rent_epoch..=self.epoch) .map(|epoch| self.epoch_schedule.get_slots_in_epoch(epoch + 1)) @@ -53,13 +53,13 @@ impl RentCollector { if account.lamports > rent_due { account.rent_epoch = self.epoch + 1; account.lamports -= rent_due; - Some((account, rent_due)) + (Some(account), rent_due) } else { - None + (None, account.lamports) } } else { // maybe collect rent later, leave account alone - Some((account, 0)) + (Some(account), 0) } } }