From 2db9bb58822843d9d6f322fb8bdec11be1f829c2 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Wed, 21 Aug 2024 12:49:18 -0700 Subject: [PATCH] SVM: add new `solana-svm-rent-collector` crate --- Cargo.lock | 7 + Cargo.toml | 2 + svm-rent-collector/Cargo.toml | 13 + svm-rent-collector/src/lib.rs | 6 + svm-rent-collector/src/rent_state.rs | 15 ++ svm-rent-collector/src/svm_rent_collector.rs | 137 ++++++++++ .../src/svm_rent_collector/rent_collector.rs | 255 ++++++++++++++++++ 7 files changed, 435 insertions(+) create mode 100644 svm-rent-collector/Cargo.toml create mode 100644 svm-rent-collector/src/lib.rs create mode 100644 svm-rent-collector/src/rent_state.rs create mode 100644 svm-rent-collector/src/svm_rent_collector.rs create mode 100644 svm-rent-collector/src/svm_rent_collector/rent_collector.rs diff --git a/Cargo.lock b/Cargo.lock index 90d37815cebba9..d244a91d6ac653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7746,6 +7746,13 @@ dependencies = [ "termcolor", ] +[[package]] +name = "solana-svm-rent-collector" +version = "2.1.0" +dependencies = [ + "solana-sdk", +] + [[package]] name = "solana-svm-transaction" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2a772d3ee85d3d..ae3e93fd024d12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ members = [ "streamer", "svm", "svm-conformance", + "svm-rent-collector", "svm-transaction", "svm/examples/paytube", "test-validator", @@ -434,6 +435,7 @@ solana-streamer = { path = "streamer", version = "=2.1.0" } solana-svm = { path = "svm", version = "=2.1.0" } solana-svm-conformance = { path = "svm-conformance", version = "=2.1.0" } solana-svm-example-paytube = { path = "svm/examples/paytube", version = "=2.1.0" } +solana-svm-rent-collector = { path = "svm-rent-collector", version = "=2.1.0" } solana-svm-transaction = { path = "svm-transaction", version = "=2.1.0" } solana-system-program = { path = "programs/system", version = "=2.1.0" } solana-test-validator = { path = "test-validator", version = "=2.1.0" } diff --git a/svm-rent-collector/Cargo.toml b/svm-rent-collector/Cargo.toml new file mode 100644 index 00000000000000..426f06593a3eac --- /dev/null +++ b/svm-rent-collector/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "solana-svm-rent-collector" +description = "Solana SVM Rent Collector" +documentation = "https://docs.rs/solana-svm-rent-collector" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +solana-sdk = { workspace = true } diff --git a/svm-rent-collector/src/lib.rs b/svm-rent-collector/src/lib.rs new file mode 100644 index 00000000000000..5038410e9e9f6f --- /dev/null +++ b/svm-rent-collector/src/lib.rs @@ -0,0 +1,6 @@ +//! Solana SVM Rent Collector. +//! +//! Rent management for SVM. + +pub mod rent_state; +pub mod svm_rent_collector; diff --git a/svm-rent-collector/src/rent_state.rs b/svm-rent-collector/src/rent_state.rs new file mode 100644 index 00000000000000..f4ec54a0532a24 --- /dev/null +++ b/svm-rent-collector/src/rent_state.rs @@ -0,0 +1,15 @@ +//! Account rent state. + +/// Rent state of a Solana account. +#[derive(Debug, PartialEq, Eq)] +pub enum RentState { + /// account.lamports == 0 + Uninitialized, + /// 0 < account.lamports < rent-exempt-minimum + RentPaying { + lamports: u64, // account.lamports() + data_size: usize, // account.data().len() + }, + /// account.lamports >= rent-exempt-minimum + RentExempt, +} diff --git a/svm-rent-collector/src/svm_rent_collector.rs b/svm-rent-collector/src/svm_rent_collector.rs new file mode 100644 index 00000000000000..6decf8fbe6a36c --- /dev/null +++ b/svm-rent-collector/src/svm_rent_collector.rs @@ -0,0 +1,137 @@ +//! Plugin trait for rent collection within the Solana SVM. + +use { + crate::rent_state::RentState, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount}, + clock::Epoch, + pubkey::Pubkey, + rent::{Rent, RentDue}, + rent_collector::CollectedInfo, + transaction::{Result, TransactionError}, + transaction_context::{IndexOfAccount, TransactionContext}, + }, +}; + +mod rent_collector; + +/// Rent collector trait. Represents an entity that can evaluate the rent state +/// of an account, determine rent due, and collect rent. +/// +/// Implementors are responsible for evaluating rent due and collecting rent +/// from accounts, if required. Methods for evaluating account rent state have +/// default implementations, which can be overridden for customized rent +/// management. +pub trait SVMRentCollector { + /// Check rent state transition for an account in a transaction. + /// + /// This method has a default implementation that calls into + /// `check_rent_state_with_account`. + fn check_rent_state( + &self, + pre_rent_state: Option<&RentState>, + post_rent_state: Option<&RentState>, + transaction_context: &TransactionContext, + index: IndexOfAccount, + ) -> 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"; + self.check_rent_state_with_account( + pre_rent_state, + post_rent_state, + transaction_context + .get_key_of_account_at_index(index) + .expect(expect_msg), + &transaction_context + .get_account_at_index(index) + .expect(expect_msg) + .borrow(), + index, + )?; + } + Ok(()) + } + + /// Check rent state transition for an account directly. + /// + /// This method has a default implementation that checks whether the + /// transition is allowed and returns an error if it is not. It also + /// verifies that the account is not the incinerator. + fn check_rent_state_with_account( + &self, + pre_rent_state: &RentState, + post_rent_state: &RentState, + address: &Pubkey, + _account_state: &AccountSharedData, + account_index: IndexOfAccount, + ) -> Result<()> { + if !solana_sdk::incinerator::check_id(address) + && !self.transition_allowed(pre_rent_state, post_rent_state) + { + let account_index = account_index as u8; + Err(TransactionError::InsufficientFundsForRent { account_index }) + } else { + Ok(()) + } + } + + /// Collect rent from an account. + fn collect_rent(&self, address: &Pubkey, account: &mut AccountSharedData) -> CollectedInfo; + + /// Determine the rent state of an account. + /// + /// This method has a default implementation that treats accounts with zero + /// lamports as uninitialized and uses the implemented `get_rent` to + /// determine whether an account is rent-exempt. + fn get_account_rent_state(&self, account: &AccountSharedData) -> RentState { + if account.lamports() == 0 { + RentState::Uninitialized + } else if self + .get_rent() + .is_exempt(account.lamports(), account.data().len()) + { + RentState::RentExempt + } else { + RentState::RentPaying { + data_size: account.data().len(), + lamports: account.lamports(), + } + } + } + + /// Get the rent collector's rent instance. + fn get_rent(&self) -> &Rent; + + /// Get the rent due for an account. + fn get_rent_due(&self, lamports: u64, data_len: usize, account_rent_epoch: Epoch) -> RentDue; + + /// Check whether a transition from the pre_rent_state to the + /// post_rent_state is valid. + /// + /// This method has a default implementation that allows transitions from + /// any state to `RentState::Uninitialized` or `RentState::RentExempt`. + /// Pre-state `RentState::RentPaying` can only transition to + /// `RentState::RentPaying` if the data size remains the same and the + /// account is not credited. + fn transition_allowed(&self, pre_rent_state: &RentState, post_rent_state: &RentState) -> bool { + match post_rent_state { + RentState::Uninitialized | RentState::RentExempt => true, + RentState::RentPaying { + data_size: post_data_size, + lamports: post_lamports, + } => { + match pre_rent_state { + RentState::Uninitialized | RentState::RentExempt => false, + RentState::RentPaying { + data_size: pre_data_size, + lamports: pre_lamports, + } => { + // Cannot remain RentPaying if resized or credited. + post_data_size == pre_data_size && post_lamports <= pre_lamports + } + } + } + } + } +} diff --git a/svm-rent-collector/src/svm_rent_collector/rent_collector.rs b/svm-rent-collector/src/svm_rent_collector/rent_collector.rs new file mode 100644 index 00000000000000..610bb4c63f1ca5 --- /dev/null +++ b/svm-rent-collector/src/svm_rent_collector/rent_collector.rs @@ -0,0 +1,255 @@ +//! Implementation of `SVMRentCollector` for `RentCollector` from the Solana +//! SDK. + +use { + crate::svm_rent_collector::SVMRentCollector, + solana_sdk::{ + account::AccountSharedData, + clock::Epoch, + pubkey::Pubkey, + rent::{Rent, RentDue}, + rent_collector::{CollectedInfo, RentCollector}, + }, +}; + +impl SVMRentCollector for RentCollector { + fn collect_rent(&self, address: &Pubkey, account: &mut AccountSharedData) -> CollectedInfo { + self.collect_from_existing_account(address, account) + } + + fn get_rent(&self) -> &Rent { + &self.rent + } + + fn get_rent_due(&self, lamports: u64, data_len: usize, account_rent_epoch: Epoch) -> RentDue { + self.get_rent_due(lamports, data_len, account_rent_epoch) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::rent_state::RentState, + solana_sdk::{ + account::ReadableAccount, + clock::Epoch, + epoch_schedule::EpochSchedule, + pubkey::Pubkey, + transaction::TransactionError, + transaction_context::{IndexOfAccount, TransactionContext}, + }, + }; + + #[test] + fn test_get_account_rent_state() { + let program_id = Pubkey::new_unique(); + let uninitialized_account = AccountSharedData::new(0, 0, &Pubkey::default()); + + let account_data_size = 100; + + let rent_collector = RentCollector::new( + Epoch::default(), + EpochSchedule::default(), + 0.0, + Rent::free(), + ); + + let rent_exempt_account = 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!( + rent_collector.get_account_rent_state(&uninitialized_account), + RentState::Uninitialized + ); + assert_eq!( + rent_collector.get_account_rent_state(&rent_exempt_account), + RentState::RentExempt + ); + + let rent = Rent::default(); + let rent_minimum_balance = rent.minimum_balance(account_data_size); + let rent_paying_account = AccountSharedData::new( + rent_minimum_balance.saturating_sub(1), + account_data_size, + &program_id, + ); + let rent_exempt_account = AccountSharedData::new( + rent.minimum_balance(account_data_size), + account_data_size, + &program_id, + ); + let rent_collector = + RentCollector::new(Epoch::default(), EpochSchedule::default(), 0.0, rent); + + assert_eq!( + rent_collector.get_account_rent_state(&uninitialized_account), + RentState::Uninitialized + ); + assert_eq!( + rent_collector.get_account_rent_state(&rent_paying_account), + RentState::RentPaying { + data_size: account_data_size, + lamports: rent_paying_account.lamports(), + } + ); + assert_eq!( + rent_collector.get_account_rent_state(&rent_exempt_account), + RentState::RentExempt + ); + } + + #[test] + fn test_transition_allowed() { + let rent_collector = RentCollector::default(); + + let post_rent_state = RentState::Uninitialized; + assert!(rent_collector.transition_allowed(&RentState::Uninitialized, &post_rent_state)); + assert!(rent_collector.transition_allowed(&RentState::RentExempt, &post_rent_state)); + assert!(rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + &post_rent_state + )); + + let post_rent_state = RentState::RentExempt; + assert!(rent_collector.transition_allowed(&RentState::Uninitialized, &post_rent_state)); + assert!(rent_collector.transition_allowed(&RentState::RentExempt, &post_rent_state)); + assert!(rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + &post_rent_state + )); + + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + + // These transitions are not allowed. + assert!(!rent_collector.transition_allowed(&RentState::Uninitialized, &post_rent_state)); + assert!(!rent_collector.transition_allowed(&RentState::RentExempt, &post_rent_state)); + + // Transition is not allowed if data size changes. + assert!(!rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 3, + lamports: 5, + }, + &post_rent_state + )); + assert!(!rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 1, + lamports: 5, + }, + &post_rent_state + )); + + // Transition is always allowed if there is no account data resize or + // change in account's lamports. + assert!(rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 2, + lamports: 5, + }, + &post_rent_state + )); + // Transition is always allowed if there is no account data resize and + // account's lamports is reduced. + assert!(rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 2, + lamports: 7, + }, + &post_rent_state + )); + // Transition is not allowed if the account is credited with more + // lamports and remains rent-paying. + assert!(!rent_collector.transition_allowed( + &RentState::RentPaying { + data_size: 2, + lamports: 3, + }, + &post_rent_state + )); + } + + #[test] + fn test_check_rent_state_with_account() { + let rent_collector = RentCollector::default(); + + let pre_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 3, + }; + + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + let account_index = 2 as IndexOfAccount; + let key = Pubkey::new_unique(); + let result = rent_collector.check_rent_state_with_account( + &pre_rent_state, + &post_rent_state, + &key, + &AccountSharedData::default(), + account_index, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { + account_index: account_index as u8 + }) + ); + + let result = rent_collector.check_rent_state_with_account( + &pre_rent_state, + &post_rent_state, + &solana_sdk::incinerator::id(), + &AccountSharedData::default(), + account_index, + ); + assert!(result.is_ok()); + } + + #[test] + fn test_check_rent_state() { + let rent_collector = RentCollector::default(); + + let context = TransactionContext::new( + vec![(Pubkey::new_unique(), AccountSharedData::default())], + Rent::default(), + 20, + 20, + ); + + let pre_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 3, + }; + + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + + let result = rent_collector.check_rent_state( + Some(&pre_rent_state), + Some(&post_rent_state), + &context, + 0, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + + let result = rent_collector.check_rent_state(None, Some(&post_rent_state), &context, 0); + assert!(result.is_ok()); + } +}