Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reserved_account_keys module to sdk #84

Merged
merged 3 commits into from
Mar 14, 2024
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
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,10 @@ pub mod allow_commission_decrease_at_any_time {
solana_sdk::declare_id!("decoMktMcnmiq6t3u7g5BfgcQu91nKZr6RvMYf9z1Jb");
}

pub mod add_new_reserved_account_keys {
solana_sdk::declare_id!("8U4skmMVnF6k2kMvrWbQuRUT3qQSiTYpSjqmhmgfthZu");
}

pub mod consume_blockstore_duplicate_proofs {
solana_sdk::declare_id!("6YsBCejwK96GZCkJ6mkZ4b68oP63z2PLoQmWjC7ggTqZ");
}
Expand Down Expand Up @@ -955,6 +959,7 @@ lazy_static! {
(drop_legacy_shreds::id(), "drops legacy shreds #34328"),
(allow_commission_decrease_at_any_time::id(), "Allow commission decrease at any time in epoch #33843"),
(consume_blockstore_duplicate_proofs::id(), "consume duplicate proofs from blockstore in consensus #34372"),
(add_new_reserved_account_keys::id(), "add new unwritable reserved accounts #34899"),
(index_erasure_conflict_duplicate_proofs::id(), "generate duplicate proofs for index and erasure conflicts #34360"),
(merkle_conflict_duplicate_proofs::id(), "generate duplicate proofs for merkle root conflicts #34270"),
(disable_bpf_loader_instructions::id(), "disable bpf loader management instructions #34194"),
Expand Down
1 change: 1 addition & 0 deletions sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub mod quic;
pub mod recent_blockhashes_account;
pub mod rent_collector;
pub mod rent_debits;
pub mod reserved_account_keys;
pub mod reward_info;
pub mod reward_type;
pub mod rpc_port;
Expand Down
256 changes: 256 additions & 0 deletions sdk/src/reserved_account_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
//! Collection of reserved account keys that cannot be write-locked by transactions.
//! New reserved account keys may be added as long as they specify a feature
//! gate that transitions the key into read-only at an epoch boundary.

#![cfg(feature = "full")]

use {
crate::{
address_lookup_table, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable,
compute_budget, config, ed25519_program, feature,
feature_set::{self, FeatureSet},
loader_v4, native_loader,
pubkey::Pubkey,
secp256k1_program, stake, system_program, sysvar, vote,
},
lazy_static::lazy_static,
std::collections::{HashMap, HashSet},
};

// Inline zk token program id since it isn't available in the sdk
mod zk_token_proof_program {
solana_sdk::declare_id!("ZkTokenProof1111111111111111111111111111111");
}

/// `ReservedAccountKeys` holds the set of currently active/inactive
/// account keys that are reserved by the protocol and may not be write-locked
/// during transaction processing.
#[derive(Debug, Clone, PartialEq)]
pub struct ReservedAccountKeys {
/// Set of currently active reserved account keys
pub active: HashSet<Pubkey>,
/// Set of currently inactive reserved account keys that will be moved to the
/// active set when their feature id is activated
inactive: HashMap<Pubkey, Pubkey>,
}

impl Default for ReservedAccountKeys {
fn default() -> Self {
Self::new(&RESERVED_ACCOUNTS)
}
}

impl ReservedAccountKeys {
/// Compute a set of active / inactive reserved account keys from a list of
/// keys with a designated feature id. If a reserved account key doesn't
/// designate a feature id, it's already activated and should be inserted
/// into the active set. If it does have a feature id, insert the key and
/// its feature id into the inactive map.
fn new(reserved_accounts: &[ReservedAccount]) -> Self {
Self {
active: reserved_accounts
.iter()
.filter(|reserved| reserved.feature_id.is_none())
.map(|reserved| reserved.key)
.collect(),
inactive: reserved_accounts
.iter()
.filter_map(|ReservedAccount { key, feature_id }| {
feature_id.as_ref().map(|feature_id| (*key, *feature_id))
})
.collect(),
}
}

/// Compute a set with all reserved keys active, regardless of whether their
/// feature was activated. This is not to be used by the runtime. Useful for
/// off-chain utilities that need to filter out reserved accounts.
pub fn new_all_activated() -> Self {
Self {
active: Self::all_keys_iter().copied().collect(),
inactive: HashMap::default(),
}
}

/// Returns whether the specified key is reserved
pub fn is_reserved(&self, key: &Pubkey) -> bool {
self.active.contains(key)
}

/// Move inactive reserved account keys to the active set if their feature
/// is active.
pub fn update_active_set(&mut self, feature_set: &FeatureSet) {
Copy link
Author

Choose a reason for hiding this comment

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

@CriesofCarrots this is the new method to add new reserved account keys that are activated by the feature set. It's used like this now in the bank:

        // Update active set of reserved account keys which are not allowed to be write locked
        self.reserved_account_keys = {
            let mut reserved_keys = ReservedAccountKeys::clone(&self.reserved_account_keys);
            reserved_keys.update_active_set(&self.feature_set);
            Arc::new(reserved_keys)
        };

I know you asked for methods to add / remove keys from this struct but I think those can be added later when needed. The new struct supports those operations more readily than the previous implementation

Choose a reason for hiding this comment

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

Thank you! I'm much happier with this, and I agree that add/remove methods can easily be added later, given the new design.

self.inactive.retain(|reserved_key, feature_id| {
if feature_set.is_active(feature_id) {
self.active.insert(*reserved_key);
false
} else {
true
}
});
}

/// Return an iterator over all active / inactive reserved keys. This is not
/// to be used by the runtime. Useful for off-chain utilities that need to
/// filter out reserved accounts.
pub fn all_keys_iter() -> impl Iterator<Item = &'static Pubkey> {
RESERVED_ACCOUNTS
.iter()
.map(|reserved_key| &reserved_key.key)
}

/// Return an empty set of reserved keys for visibility when using in
/// tests where the dynamic reserved key set is not available
pub fn empty_key_set() -> HashSet<Pubkey> {
HashSet::default()
}
}

/// `ReservedAccount` represents a reserved account that will not be
/// write-lockable by transactions. If a feature id is set, the account will
/// become read-only only after the feature has been activated.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct ReservedAccount {
key: Pubkey,
feature_id: Option<Pubkey>,
}

impl ReservedAccount {
fn new_pending(key: Pubkey, feature_id: Pubkey) -> Self {
Self {
key,
feature_id: Some(feature_id),
}
}

fn new_active(key: Pubkey) -> Self {
Self {
key,
feature_id: None,
}
}
}

// New reserved accounts should be added in alphabetical order and must specify
// a feature id for activation. Reserved accounts cannot be removed from this
// list without breaking consensus.
lazy_static! {
static ref RESERVED_ACCOUNTS: Vec<ReservedAccount> = [
// builtin programs
ReservedAccount::new_pending(address_lookup_table::program::id(), feature_set::add_new_reserved_account_keys::id()),
ReservedAccount::new_active(bpf_loader::id()),
ReservedAccount::new_active(bpf_loader_deprecated::id()),
ReservedAccount::new_active(bpf_loader_upgradeable::id()),
ReservedAccount::new_pending(compute_budget::id(), feature_set::add_new_reserved_account_keys::id()),
ReservedAccount::new_active(config::program::id()),
ReservedAccount::new_pending(ed25519_program::id(), feature_set::add_new_reserved_account_keys::id()),
ReservedAccount::new_active(feature::id()),
ReservedAccount::new_pending(loader_v4::id(), feature_set::add_new_reserved_account_keys::id()),
ReservedAccount::new_pending(secp256k1_program::id(), feature_set::add_new_reserved_account_keys::id()),
#[allow(deprecated)]
ReservedAccount::new_active(stake::config::id()),
ReservedAccount::new_active(stake::program::id()),
ReservedAccount::new_active(system_program::id()),
ReservedAccount::new_active(vote::program::id()),
ReservedAccount::new_pending(zk_token_proof_program::id(), feature_set::add_new_reserved_account_keys::id()),

// sysvars
ReservedAccount::new_active(sysvar::clock::id()),
ReservedAccount::new_pending(sysvar::epoch_rewards::id(), feature_set::add_new_reserved_account_keys::id()),
ReservedAccount::new_active(sysvar::epoch_schedule::id()),
#[allow(deprecated)]
ReservedAccount::new_active(sysvar::fees::id()),
ReservedAccount::new_active(sysvar::instructions::id()),
ReservedAccount::new_pending(sysvar::last_restart_slot::id(), feature_set::add_new_reserved_account_keys::id()),
#[allow(deprecated)]
ReservedAccount::new_active(sysvar::recent_blockhashes::id()),
ReservedAccount::new_active(sysvar::rent::id()),
ReservedAccount::new_active(sysvar::rewards::id()),
ReservedAccount::new_active(sysvar::slot_hashes::id()),
ReservedAccount::new_active(sysvar::slot_history::id()),
ReservedAccount::new_active(sysvar::stake_history::id()),

// other
ReservedAccount::new_active(native_loader::id()),
ReservedAccount::new_pending(sysvar::id(), feature_set::add_new_reserved_account_keys::id()),
].to_vec();
}

#[cfg(test)]
mod tests {
use {
super::*,
solana_program::{message::legacy::BUILTIN_PROGRAMS_KEYS, sysvar::ALL_IDS},
};

#[test]
fn test_is_reserved() {
let feature_id = Pubkey::new_unique();
let active_reserved_account = ReservedAccount::new_active(Pubkey::new_unique());
let pending_reserved_account =
ReservedAccount::new_pending(Pubkey::new_unique(), feature_id);
let reserved_account_keys =
ReservedAccountKeys::new(&[active_reserved_account, pending_reserved_account]);

assert!(
reserved_account_keys.is_reserved(&active_reserved_account.key),
"active reserved accounts should be inserted into the active set"
);
assert!(
!reserved_account_keys.is_reserved(&pending_reserved_account.key),
"pending reserved accounts should NOT be inserted into the active set"
);
}

#[test]
fn test_update_active_set() {
let feature_ids = [Pubkey::new_unique(), Pubkey::new_unique()];
let active_reserved_key = Pubkey::new_unique();
let pending_reserved_keys = [Pubkey::new_unique(), Pubkey::new_unique()];
let reserved_accounts = vec![
ReservedAccount::new_active(active_reserved_key),
ReservedAccount::new_pending(pending_reserved_keys[0], feature_ids[0]),
ReservedAccount::new_pending(pending_reserved_keys[1], feature_ids[1]),
];

let mut reserved_account_keys = ReservedAccountKeys::new(&reserved_accounts);
assert!(reserved_account_keys.is_reserved(&active_reserved_key));
assert!(!reserved_account_keys.is_reserved(&pending_reserved_keys[0]));
assert!(!reserved_account_keys.is_reserved(&pending_reserved_keys[1]));

// Updating the active set with a default feature set should be a no-op
let previous_reserved_account_keys = reserved_account_keys.clone();
let mut feature_set = FeatureSet::default();
reserved_account_keys.update_active_set(&feature_set);
assert_eq!(reserved_account_keys, previous_reserved_account_keys);

// Updating the active set with an activated feature should also activate
// the corresponding reserved key from inactive to active
feature_set.active.insert(feature_ids[0], 0);
reserved_account_keys.update_active_set(&feature_set);

assert!(reserved_account_keys.is_reserved(&active_reserved_key));
assert!(reserved_account_keys.is_reserved(&pending_reserved_keys[0]));
assert!(!reserved_account_keys.is_reserved(&pending_reserved_keys[1]));

// Update the active set again to ensure that the inactive map is
// properly retained
feature_set.active.insert(feature_ids[1], 0);
reserved_account_keys.update_active_set(&feature_set);

assert!(reserved_account_keys.is_reserved(&active_reserved_key));
assert!(reserved_account_keys.is_reserved(&pending_reserved_keys[0]));
assert!(reserved_account_keys.is_reserved(&pending_reserved_keys[1]));
}

#[test]
fn test_static_list_compat() {
let mut static_set = HashSet::new();
static_set.extend(ALL_IDS.iter().cloned());
static_set.extend(BUILTIN_PROGRAMS_KEYS.iter().cloned());

let initial_active_set = ReservedAccountKeys::default().active;

assert_eq!(initial_active_set, static_set);
}
}
Loading