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

Fix bad rent in Bank::deposit as if since epoch 0 #10468

Merged
merged 8 commits into from
Aug 11, 2020
Merged
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
8 changes: 8 additions & 0 deletions docs/src/implemented-proposals/rent.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Currently, the rent cost is fixed at the genesis. However, it's anticipated to b

There are two timings of collecting rent from accounts: \(1\) when referenced by a transaction, \(2\) periodically once an epoch. \(1\) includes the transaction to create the new account itself, and it happens during the normal transaction processing by the bank as part of the load phase. \(2\) exists to ensure to collect rents from stale accounts, which aren't referenced in recent epochs at all. \(2\) requires the whole scan of accounts and is spread over an epoch based on account address prefix to avoid load spikes due to this rent collection.

On the contrary, rent collection isn't applied to accounts that are directly manipulated by any of protocol-level bookkeeping processes including:

- The distribution of rent collection itself (Otherwise, it may cause recursive rent collection handling)
- The distribution of staking rewards at the start of every epoch (To reduce as much as processing spike at the start of new epoch)
- The distribution of transaction fee at the end of every epoch

Even if those processes are out of scope of rent collection, all of manipulated accounts will eventually be handled by the \(2\) mechanism.

Copy link
Contributor

Choose a reason for hiding this comment

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

This sounds good to me. It'd be great to get a comment from @rwalker-com to be sure we aren't missing anything. 🙏

Choose a reason for hiding this comment

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

lgtm, and this is consistent with what I intended, but the call flow to load and store accounts is pretty tortured, and has many paths. were the bank to document which APIs were for runtime access of accounts, it would go a long way to making these easier to reason about.

Copy link
Contributor Author

@ryoqun ryoqun Aug 11, 2020

Choose a reason for hiding this comment

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

the call flow to load and store accounts is pretty tortured ...

@rwalker-com (UPDATED) Yeah, I think so too... I'll address the documetation work along with #10426

Anyway, thanks for confirming this pr!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Err, wrong user @rwalker-apple...

## Actual processing of collecting rent

Rent is due for one epoch's worth of time, and accounts always have `Account::rent_epoch` of `current_epoch + 1`.
Expand Down
8 changes: 4 additions & 4 deletions runtime/src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ impl Accounts {
let (account, rent) =
AccountsDB::load(storage, ancestors, accounts_index, key)
.map(|(mut account, _)| {
if message.is_writable(i) && !account.executable {
let rent_due = rent_collector.update(&key, &mut account);
if message.is_writable(i) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed !account.executable because this one was fairly easy to prove it's redundant.

let rent_due = rent_collector
.collect_from_existing_account(&key, &mut account);
(account, rent_due)
} else {
(account, 0)
Expand Down Expand Up @@ -736,8 +737,7 @@ impl Accounts {
);
if message.is_writable(i) {
if account.rent_epoch == 0 {
Copy link
Contributor Author

@ryoqun ryoqun Jun 9, 2020

Choose a reason for hiding this comment

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

Well, these inconsistent predicates are concerning me. Ideally, I want to put everything into RentCollector::update/RentCollector::collect_from_existing_account(). But, to avoid any additional risks, I've punted it. ;)

account.rent_epoch = rent_collector.epoch;
acc.2 += rent_collector.update(&key, account);
acc.2 += rent_collector.collect_from_created_account(&key, account);
}
accounts.push((key, &*account));
}
Expand Down
40 changes: 26 additions & 14 deletions runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1155,18 +1155,13 @@ impl Bank {
.unwrap()
.genesis_hash(&genesis_config.hash(), &self.fee_calculator);

self.hashes_per_tick = genesis_config.poh_config.hashes_per_tick;
self.ticks_per_slot = genesis_config.ticks_per_slot;
self.ns_per_slot = genesis_config.poh_config.target_tick_duration.as_nanos()
* genesis_config.ticks_per_slot as u128;
self.hashes_per_tick = genesis_config.hashes_per_tick();
self.ticks_per_slot = genesis_config.ticks_per_slot();
self.ns_per_slot = genesis_config.ns_per_slot();
self.genesis_creation_time = genesis_config.creation_time;
self.unused = genesis_config.unused;
self.max_tick_height = (self.slot + 1) * self.ticks_per_slot;
self.slots_per_year = years_as_slots(
1.0,
&genesis_config.poh_config.target_tick_duration,
self.ticks_per_slot,
);
self.slots_per_year = genesis_config.slots_per_year();

self.epoch_schedule = genesis_config.epoch_schedule;

Expand Down Expand Up @@ -2064,7 +2059,9 @@ impl Bank {
// parallelize?
let mut rent = 0;
for (pubkey, mut account) in accounts {
rent += self.rent_collector.update(&pubkey, &mut account);
rent += self
.rent_collector
.collect_from_existing_account(&pubkey, &mut account);
// Store all of them unconditionally to purge old AppendVec,
// even if collected rent is 0 (= not updated).
self.store_account(&pubkey, &account);
Expand Down Expand Up @@ -2501,10 +2498,25 @@ impl Bank {

pub fn deposit(&self, pubkey: &Pubkey, lamports: u64) {
let mut account = self.get_account(pubkey).unwrap_or_default();
self.collected_rent.fetch_add(
self.rent_collector.update(pubkey, &mut account),
Ordering::Relaxed,
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, there were two bugs..:

  1. When a new account is created via .deposit() at epoch N, its rent is calculated since epoch 0. (this was the original bug this pr fixed)
  2. Also, the exact timing of the rent collection is wrong: we should add lamports then collect rents inside this fn. self.rent_collector.update(Account::default()) (= new account case) doesn't collect rents. And it effectively stores the account without collecting rent, it only adds lamports. So actual rent collection happens the next time the account is touched.. (I've found this bug today..)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is fixed by not doing rent collection at all as the new behavior.


let should_be_in_new_behavior = match self.operating_mode() {
OperatingMode::Development => true,
OperatingMode::Preview => self.epoch() >= Epoch::max_value(),
OperatingMode::Stable => self.epoch() >= Epoch::max_value(),
};

// don't collect rents if we're in the new behavior;
// in genral, it's not worthwhile to account for rents outside the runtime (transactions)
// there are too many and subtly nuanced modification codepaths
if !should_be_in_new_behavior {
// previously we're too much collecting rents as if it existed since epoch 0...
self.collected_rent.fetch_add(
self.rent_collector
.collect_from_existing_account(pubkey, &mut account),
Ordering::Relaxed,
);
}

account.lamports += lamports;
self.store_account(pubkey, &account);
}
Expand Down
65 changes: 61 additions & 4 deletions runtime/src/rent_collector.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
//! calculate and collect rent from Accounts
use solana_sdk::{
account::Account, clock::Epoch, epoch_schedule::EpochSchedule, incinerator, pubkey::Pubkey,
rent::Rent, sysvar,
account::Account, clock::Epoch, epoch_schedule::EpochSchedule, genesis_config::GenesisConfig,
incinerator, pubkey::Pubkey, rent::Rent, sysvar,
};

#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Debug, AbiExample)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, AbiExample)]
pub struct RentCollector {
pub epoch: Epoch,
pub epoch_schedule: EpochSchedule,
pub slots_per_year: f64,
pub rent: Rent,
}

impl Default for RentCollector {
fn default() -> Self {
Self {
epoch: Epoch::default(),
epoch_schedule: EpochSchedule::default(),
// derive default value using GenesisConfig::default()
slots_per_year: GenesisConfig::default().slots_per_year(),
rent: Rent::default(),
}
}
}

impl RentCollector {
pub fn new(
epoch: Epoch,
Expand All @@ -36,7 +48,8 @@ impl RentCollector {
// updates this account's lamports and status and returns
// the account rent collected, if any
//
pub fn update(&self, address: &Pubkey, account: &mut Account) -> u64 {
#[must_use = "add to Bank::collected_rent"]
pub fn collect_from_existing_account(&self, address: &Pubkey, account: &mut Account) -> u64 {
if account.executable
|| account.rent_epoch > self.epoch
|| sysvar::check_id(&account.owner)
Expand Down Expand Up @@ -75,4 +88,48 @@ impl RentCollector {
}
}
}

#[must_use = "add to Bank::collected_rent"]
pub fn collect_from_created_account(&self, address: &Pubkey, account: &mut Account) -> u64 {
// initialize rent_epoch as created at this epoch
account.rent_epoch = self.epoch;
self.collect_from_existing_account(address, account)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_collect_from_account_created_and_existing() {
let old_lamports = 1000;
let old_epoch = 1;
let new_epoch = 3;

let (mut created_account, mut existing_account) = {
let mut account = Account::default();
account.lamports = old_lamports;
account.rent_epoch = old_epoch;

(account.clone(), account)
};

let rent_collector = RentCollector::default().clone_with_epoch(new_epoch);

let collected =
rent_collector.collect_from_created_account(&Pubkey::new_rand(), &mut created_account);
assert!(created_account.lamports < old_lamports);
assert_eq!(created_account.lamports + collected, old_lamports);
assert_ne!(created_account.rent_epoch, old_epoch);

let collected = rent_collector
.collect_from_existing_account(&Pubkey::new_rand(), &mut existing_account);
assert!(existing_account.lamports < old_lamports);
assert_eq!(existing_account.lamports + collected, old_lamports);
assert_ne!(existing_account.rent_epoch, old_epoch);

assert!(created_account.lamports > existing_account.lamports);
assert_eq!(created_account.rent_epoch, existing_account.rent_epoch);
}
}
21 changes: 21 additions & 0 deletions sdk/src/genesis_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
shred_version::compute_shred_version,
signature::{Keypair, Signer},
system_program,
timing::years_as_slots,
};
use bincode::{deserialize, serialize};
use chrono::{TimeZone, Utc};
Expand Down Expand Up @@ -183,6 +184,26 @@ impl GenesisConfig {
pub fn add_rewards_pool(&mut self, pubkey: Pubkey, account: Account) {
self.rewards_pools.insert(pubkey, account);
}

pub fn hashes_per_tick(&self) -> Option<u64> {
self.poh_config.hashes_per_tick
}

pub fn ticks_per_slot(&self) -> u64 {
self.ticks_per_slot
}

pub fn ns_per_slot(&self) -> u128 {
self.poh_config.target_tick_duration.as_nanos() * self.ticks_per_slot() as u128
}

pub fn slots_per_year(&self) -> f64 {
years_as_slots(
1.0,
&self.poh_config.target_tick_duration,
self.ticks_per_slot(),
)
}
}

impl fmt::Display for GenesisConfig {
Expand Down