From 7601935094fbfd51bd9a9cc7474bb59f1d335d13 Mon Sep 17 00:00:00 2001 From: behzad nouri Date: Fri, 15 Jul 2022 13:23:37 +0000 Subject: [PATCH 1/2] restricts rent-paying accounts lifetime extension (#26606) https://github.com/solana-labs/solana/pull/22292 prevents rent paying accounts creation going forward. However a rent paying account can linger on for ever if it is continually topped up but stays below the rent-exempt minimum. This can prevent eliminating accounts-rewrites and the problematic rent_epoch field in accounts. Link to discord discussion: https://discord.com/channels/428295358100013066/943609352068145162/995202300001927219 This commit restricts rent-paying accounts lifetime extension by preventing increasing lamports on the account if the account stays below the rent-exempt minimum. (cherry picked from commit bf225bae738c071bd02ec79ec0b8ddfc1679a766) # Conflicts: # runtime/src/account_rent_state.rs # runtime/src/accounts.rs # runtime/src/bank/transaction_account_state_info.rs # sdk/src/feature_set.rs --- runtime/src/account_rent_state.rs | 166 ++++++++++++++++-- runtime/src/accounts.rs | 6 + .../bank/transaction_account_state_info.rs | 8 + sdk/src/feature_set.rs | 21 +++ 4 files changed, 190 insertions(+), 11 deletions(-) diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs index 6df6a4b059501e..136e70c3f0bbc3 100644 --- a/runtime/src/account_rent_state.rs +++ b/runtime/src/account_rent_state.rs @@ -14,8 +14,10 @@ pub(crate) enum RentState { /// account.lamports == 0 Uninitialized, /// 0 < account.lamports < rent-exempt-minimum - /// Parameter is the size of the account data - RentPaying(usize), + RentPaying { + lamports: u64, // account.lamports() + data_size: usize, // account.data().len() + }, /// account.lamports >= rent-exempt-minimum RentExempt, } @@ -24,16 +26,20 @@ impl RentState { pub(crate) fn from_account(account: &AccountSharedData, rent: &Rent) -> Self { if account.lamports() == 0 { Self::Uninitialized - } else if !rent.is_exempt(account.lamports(), account.data().len()) { - Self::RentPaying(account.data().len()) - } else { + } else if rent.is_exempt(account.lamports(), account.data().len()) { Self::RentExempt + } else { + Self::RentPaying { + data_size: account.data().len(), + lamports: account.lamports(), + } } } pub(crate) fn transition_allowed_from( &self, pre_rent_state: &RentState, +<<<<<<< HEAD do_support_realloc: bool, ) -> bool { if let Self::RentPaying(post_data_size) = self { @@ -45,22 +51,47 @@ impl RentState { } } else { false // Only RentPaying can continue to be RentPaying +======= + prevent_crediting_accounts_that_end_rent_paying: bool, + ) -> bool { + match self { + Self::Uninitialized | Self::RentExempt => true, + Self::RentPaying { + data_size: post_data_size, + lamports: post_lamports, + } => { + match pre_rent_state { + Self::Uninitialized | Self::RentExempt => false, + Self::RentPaying { + data_size: pre_data_size, + lamports: pre_lamports, + } => { + // Cannot remain RentPaying if resized + if post_data_size != pre_data_size { + false + } else if prevent_crediting_accounts_that_end_rent_paying { + // Cannot remain RentPaying if credited + post_lamports <= pre_lamports + } else { + true + } + } + } +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } - } 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); } _ => {} @@ -74,6 +105,7 @@ pub(crate) fn check_rent_state( index: usize, do_support_realloc: bool, include_account_index_in_err: bool, + prevent_crediting_accounts_that_end_rent_paying: 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"; @@ -89,6 +121,7 @@ pub(crate) fn check_rent_state( .borrow(), do_support_realloc, include_account_index_in_err.then(|| index), + prevent_crediting_accounts_that_end_rent_paying, )?; } Ok(()) @@ -101,10 +134,18 @@ pub(crate) fn check_rent_state_with_account( account_state: &AccountSharedData, do_support_realloc: bool, account_index: Option, + prevent_crediting_accounts_that_end_rent_paying: bool, ) -> Result<()> { submit_rent_state_metrics(pre_rent_state, post_rent_state); if !solana_sdk::incinerator::check_id(address) +<<<<<<< HEAD && !post_rent_state.transition_allowed_from(pre_rent_state, do_support_realloc) +======= + && !post_rent_state.transition_allowed_from( + pre_rent_state, + prevent_crediting_accounts_that_end_rent_paying, + ) +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) { debug!( "Account {} not rent exempt, state {:?}", @@ -163,7 +204,10 @@ mod tests { ); assert_eq!( RentState::from_account(&rent_paying_account, &rent), - RentState::RentPaying(account_data_size) + RentState::RentPaying { + data_size: account_data_size, + lamports: rent_paying_account.lamports(), + } ); assert_eq!( RentState::from_account(&rent_exempt_account, &rent), @@ -173,7 +217,17 @@ mod tests { #[test] fn test_transition_allowed_from() { + check_transition_allowed_from( + /*prevent_crediting_accounts_that_end_rent_paying:*/ false, + ); + check_transition_allowed_from( + /*prevent_crediting_accounts_that_end_rent_paying:*/ true, + ); + } + + fn check_transition_allowed_from(prevent_crediting_accounts_that_end_rent_paying: bool) { let post_rent_state = RentState::Uninitialized; +<<<<<<< HEAD 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)); @@ -189,5 +243,95 @@ mod tests { 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)); +======= + assert!(post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentExempt, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + prevent_crediting_accounts_that_end_rent_paying + )); + + let post_rent_state = RentState::RentExempt; + assert!(post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentExempt, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 0, + lamports: 1, + }, + prevent_crediting_accounts_that_end_rent_paying + )); + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + assert!(!post_rent_state.transition_allowed_from( + &RentState::Uninitialized, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentExempt, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 3, + lamports: 5 + }, + prevent_crediting_accounts_that_end_rent_paying + )); + assert!(!post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 1, + lamports: 5 + }, + prevent_crediting_accounts_that_end_rent_paying + )); + // Transition is always allowed if there is no account data resize or + // change in account's lamports. + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 5 + }, + prevent_crediting_accounts_that_end_rent_paying + )); + // Transition is always allowed if there is no account data resize and + // account's lamports is reduced. + assert!(post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 7 + }, + prevent_crediting_accounts_that_end_rent_paying + )); + // Once the feature is activated, transition is not allowed if the + // account is credited with more lamports and remains rent-paying. + assert_eq!( + post_rent_state.transition_allowed_from( + &RentState::RentPaying { + data_size: 2, + lamports: 3 + }, + prevent_crediting_accounts_that_end_rent_paying + ), + !prevent_crediting_accounts_that_end_rent_paying + ); +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } } diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index f7d45a4ab0b324..b0aca0936c9a20 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -436,6 +436,7 @@ impl Accounts { feature_set .is_active(&feature_set::include_account_index_in_rent_error::ID) .then(|| payer_index), +<<<<<<< HEAD ); // 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 @@ -444,6 +445,11 @@ impl Accounts { rent_state_result?; } Ok(()) +======= + feature_set + .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::id()), + ) +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } fn load_executable_accounts( diff --git a/runtime/src/bank/transaction_account_state_info.rs b/runtime/src/bank/transaction_account_state_info.rs index eafe1b498fc3db..bcf2322e625306 100644 --- a/runtime/src/bank/transaction_account_state_info.rs +++ b/runtime/src/bank/transaction_account_state_info.rs @@ -64,6 +64,9 @@ impl Bank { let include_account_index_in_err = self .feature_set .is_active(&feature_set::include_account_index_in_rent_error::id()); + let prevent_crediting_accounts_that_end_rent_paying = self + .feature_set + .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::id()); for (i, (pre_state_info, post_state_info)) in pre_state_infos.iter().zip(post_state_infos).enumerate() { @@ -74,6 +77,7 @@ impl Bank { i, do_support_realloc, include_account_index_in_err, +<<<<<<< HEAD ) { // 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 @@ -82,6 +86,10 @@ impl Bank { return Err(err); } } +======= + prevent_crediting_accounts_that_end_rent_paying, + )?; +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } Ok(()) } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index ef3aeb034fb00a..b6d380e9f94e40 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -407,6 +407,21 @@ pub mod preserve_rent_epoch_for_rent_exempt_accounts { solana_sdk::declare_id!("HH3MUYReL2BvqqA3oEcAa7txju5GY6G4nxJ51zvsEjEZ"); } +<<<<<<< HEAD +======= +pub mod enable_bpf_loader_extend_program_data_ix { + solana_sdk::declare_id!("8Zs9W7D9MpSEtUWSQdGniZk2cNmV22y6FLJwCx53asme"); +} + +pub mod enable_early_verification_of_account_modifications { + solana_sdk::declare_id!("7Vced912WrRnfjaiKRiNBcbuFw7RrnLv3E3z95Y4GTNc"); +} + +pub mod prevent_crediting_accounts_that_end_rent_paying { + solana_sdk::declare_id!("812kqX67odAp5NFwM8D2N24cku7WTm9CHUTFUXaDkWPn"); +} + +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -503,6 +518,12 @@ lazy_static! { (vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"), (cap_accounts_data_size_per_block::id(), "cap the accounts data size per block #25517"), (preserve_rent_epoch_for_rent_exempt_accounts::id(), "preserve rent epoch for rent exempt accounts #26479"), +<<<<<<< HEAD +======= + (enable_bpf_loader_extend_program_data_ix::id(), "enable bpf upgradeable loader ExtendProgramData instruction #25234"), + (enable_early_verification_of_account_modifications::id(), "enable early verification of account modifications #25899"), + (prevent_crediting_accounts_that_end_rent_paying::id(), "prevent crediting rent paying accounts #26606"), +>>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter() From a1fb85a8208ad2bd5ac6f840432146e6a2b748e9 Mon Sep 17 00:00:00 2001 From: behzad nouri Date: Fri, 15 Jul 2022 09:32:54 -0400 Subject: [PATCH 2/2] removes mergify merge conflicts --- runtime/src/account_rent_state.rs | 52 ++++++------------- runtime/src/accounts.rs | 8 +-- .../bank/transaction_account_state_info.rs | 6 +-- sdk/src/feature_set.rs | 16 ------ 4 files changed, 19 insertions(+), 63 deletions(-) diff --git a/runtime/src/account_rent_state.rs b/runtime/src/account_rent_state.rs index 136e70c3f0bbc3..810b84ba18c41a 100644 --- a/runtime/src/account_rent_state.rs +++ b/runtime/src/account_rent_state.rs @@ -39,19 +39,7 @@ impl RentState { pub(crate) fn transition_allowed_from( &self, pre_rent_state: &RentState, -<<<<<<< HEAD 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 -======= prevent_crediting_accounts_that_end_rent_paying: bool, ) -> bool { match self { @@ -67,7 +55,7 @@ impl RentState { lamports: pre_lamports, } => { // Cannot remain RentPaying if resized - if post_data_size != pre_data_size { + if do_support_realloc && post_data_size != pre_data_size { false } else if prevent_crediting_accounts_that_end_rent_paying { // Cannot remain RentPaying if credited @@ -77,7 +65,6 @@ impl RentState { } } } ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } } } @@ -138,14 +125,11 @@ pub(crate) fn check_rent_state_with_account( ) -> Result<()> { submit_rent_state_metrics(pre_rent_state, post_rent_state); if !solana_sdk::incinerator::check_id(address) -<<<<<<< HEAD - && !post_rent_state.transition_allowed_from(pre_rent_state, do_support_realloc) -======= && !post_rent_state.transition_allowed_from( pre_rent_state, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying, ) ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) { debug!( "Account {} not rent exempt, state {:?}", @@ -226,30 +210,16 @@ mod tests { } fn check_transition_allowed_from(prevent_crediting_accounts_that_end_rent_paying: bool) { + let do_support_realloc = true; let post_rent_state = RentState::Uninitialized; -<<<<<<< HEAD - 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)); -======= assert!(post_rent_state.transition_allowed_from( &RentState::Uninitialized, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(post_rent_state.transition_allowed_from( &RentState::RentExempt, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(post_rent_state.transition_allowed_from( @@ -257,16 +227,19 @@ mod tests { data_size: 0, lamports: 1, }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); let post_rent_state = RentState::RentExempt; assert!(post_rent_state.transition_allowed_from( &RentState::Uninitialized, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(post_rent_state.transition_allowed_from( &RentState::RentExempt, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(post_rent_state.transition_allowed_from( @@ -274,6 +247,7 @@ mod tests { data_size: 0, lamports: 1, }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); let post_rent_state = RentState::RentPaying { @@ -282,10 +256,12 @@ mod tests { }; assert!(!post_rent_state.transition_allowed_from( &RentState::Uninitialized, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(!post_rent_state.transition_allowed_from( &RentState::RentExempt, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(!post_rent_state.transition_allowed_from( @@ -293,6 +269,7 @@ mod tests { data_size: 3, lamports: 5 }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); assert!(!post_rent_state.transition_allowed_from( @@ -300,6 +277,7 @@ mod tests { data_size: 1, lamports: 5 }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); // Transition is always allowed if there is no account data resize or @@ -309,6 +287,7 @@ mod tests { data_size: 2, lamports: 5 }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); // Transition is always allowed if there is no account data resize and @@ -318,6 +297,7 @@ mod tests { data_size: 2, lamports: 7 }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying )); // Once the feature is activated, transition is not allowed if the @@ -328,10 +308,10 @@ mod tests { data_size: 2, lamports: 3 }, + do_support_realloc, prevent_crediting_accounts_that_end_rent_paying ), !prevent_crediting_accounts_that_end_rent_paying ); ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } } diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index b0aca0936c9a20..01a80d2325b486 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -436,7 +436,8 @@ impl Accounts { feature_set .is_active(&feature_set::include_account_index_in_rent_error::ID) .then(|| payer_index), -<<<<<<< HEAD + feature_set + .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::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 @@ -445,11 +446,6 @@ impl Accounts { rent_state_result?; } Ok(()) -======= - feature_set - .is_active(&feature_set::prevent_crediting_accounts_that_end_rent_paying::id()), - ) ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } fn load_executable_accounts( diff --git a/runtime/src/bank/transaction_account_state_info.rs b/runtime/src/bank/transaction_account_state_info.rs index bcf2322e625306..bf41b9213b6452 100644 --- a/runtime/src/bank/transaction_account_state_info.rs +++ b/runtime/src/bank/transaction_account_state_info.rs @@ -77,7 +77,7 @@ impl Bank { i, do_support_realloc, include_account_index_in_err, -<<<<<<< HEAD + prevent_crediting_accounts_that_end_rent_paying, ) { // 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 @@ -86,10 +86,6 @@ impl Bank { return Err(err); } } -======= - prevent_crediting_accounts_that_end_rent_paying, - )?; ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) } Ok(()) } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index b6d380e9f94e40..9ed7fade597075 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -407,21 +407,10 @@ pub mod preserve_rent_epoch_for_rent_exempt_accounts { solana_sdk::declare_id!("HH3MUYReL2BvqqA3oEcAa7txju5GY6G4nxJ51zvsEjEZ"); } -<<<<<<< HEAD -======= -pub mod enable_bpf_loader_extend_program_data_ix { - solana_sdk::declare_id!("8Zs9W7D9MpSEtUWSQdGniZk2cNmV22y6FLJwCx53asme"); -} - -pub mod enable_early_verification_of_account_modifications { - solana_sdk::declare_id!("7Vced912WrRnfjaiKRiNBcbuFw7RrnLv3E3z95Y4GTNc"); -} - pub mod prevent_crediting_accounts_that_end_rent_paying { solana_sdk::declare_id!("812kqX67odAp5NFwM8D2N24cku7WTm9CHUTFUXaDkWPn"); } ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -518,12 +507,7 @@ lazy_static! { (vote_authorize_with_seed::id(), "An instruction you can use to change a vote accounts authority when the current authority is a derived key #25860"), (cap_accounts_data_size_per_block::id(), "cap the accounts data size per block #25517"), (preserve_rent_epoch_for_rent_exempt_accounts::id(), "preserve rent epoch for rent exempt accounts #26479"), -<<<<<<< HEAD -======= - (enable_bpf_loader_extend_program_data_ix::id(), "enable bpf upgradeable loader ExtendProgramData instruction #25234"), - (enable_early_verification_of_account_modifications::id(), "enable early verification of account modifications #25899"), (prevent_crediting_accounts_that_end_rent_paying::id(), "prevent crediting rent paying accounts #26606"), ->>>>>>> bf225bae7 (restricts rent-paying accounts lifetime extension (#26606)) /*************** ADD NEW FEATURES HERE ***************/ ] .iter()