Bank: Add function to replace empty account with upgradeable program on feature activation (#32783)
* replace program account * modify for all cases * remove non-data swap * address tests & conditional feedback * get the rent involved * mix in owner & executable * feature-related cases * stripped back to feature-specific case only * added feature * address initial feedback * added more lamport checks * condense tests * using test_case * add fail cases to tests * more cleanup * add verifiably built program * update program account state * cleaned up serializing logic * use full word capitalization * rename old & new to dst & src * swap src and dst in parameters * add warnings and errors * rename feature to programify * test suite description clarity * remove strings from datapoints * spell out source and destination * more verbose comments in account replace functions * move lamport calculation * swap lamport check for state check * move replace functions to helper module * make replace_account methods fallible * refactor error handling * add test for source program state
This commit is contained in:
parent
5a9956824f
commit
25460f76e7
|
@ -42,6 +42,7 @@ use {
|
||||||
builtins::{BuiltinPrototype, BUILTINS},
|
builtins::{BuiltinPrototype, BUILTINS},
|
||||||
epoch_rewards_hasher::hash_rewards_into_partitions,
|
epoch_rewards_hasher::hash_rewards_into_partitions,
|
||||||
epoch_stakes::{EpochStakes, NodeVoteAccounts},
|
epoch_stakes::{EpochStakes, NodeVoteAccounts},
|
||||||
|
inline_feature_gate_program,
|
||||||
runtime_config::RuntimeConfig,
|
runtime_config::RuntimeConfig,
|
||||||
serde_snapshot::BankIncrementalSnapshotPersistence,
|
serde_snapshot::BankIncrementalSnapshotPersistence,
|
||||||
snapshot_hash::SnapshotHash,
|
snapshot_hash::SnapshotHash,
|
||||||
|
@ -215,6 +216,7 @@ pub mod bank_hash_details;
|
||||||
mod builtin_programs;
|
mod builtin_programs;
|
||||||
pub mod epoch_accounts_hash_utils;
|
pub mod epoch_accounts_hash_utils;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
|
mod replace_account;
|
||||||
mod serde_snapshot;
|
mod serde_snapshot;
|
||||||
mod sysvar_cache;
|
mod sysvar_cache;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -8054,6 +8056,24 @@ impl Bank {
|
||||||
if new_feature_activations.contains(&feature_set::update_hashes_per_tick::id()) {
|
if new_feature_activations.contains(&feature_set::update_hashes_per_tick::id()) {
|
||||||
self.apply_updated_hashes_per_tick(DEFAULT_HASHES_PER_TICK);
|
self.apply_updated_hashes_per_tick(DEFAULT_HASHES_PER_TICK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if new_feature_activations.contains(&feature_set::programify_feature_gate_program::id()) {
|
||||||
|
let datapoint_name = "bank-progamify_feature_gate_program";
|
||||||
|
if let Err(e) = replace_account::replace_empty_account_with_upgradeable_program(
|
||||||
|
self,
|
||||||
|
&feature::id(),
|
||||||
|
&inline_feature_gate_program::noop_program::id(),
|
||||||
|
datapoint_name,
|
||||||
|
) {
|
||||||
|
warn!(
|
||||||
|
"{}: Failed to replace empty account {} with upgradeable program: {}",
|
||||||
|
datapoint_name,
|
||||||
|
feature::id(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
datapoint_warn!(datapoint_name, ("slot", self.slot(), i64),);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_updated_hashes_per_tick(&mut self, hashes_per_tick: u64) {
|
fn apply_updated_hashes_per_tick(&mut self, hashes_per_tick: u64) {
|
||||||
|
@ -8196,42 +8216,6 @@ impl Bank {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use to replace programs by feature activation
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn replace_program_account(
|
|
||||||
&mut self,
|
|
||||||
old_address: &Pubkey,
|
|
||||||
new_address: &Pubkey,
|
|
||||||
datapoint_name: &'static str,
|
|
||||||
) {
|
|
||||||
if let Some(old_account) = self.get_account_with_fixed_root(old_address) {
|
|
||||||
if let Some(new_account) = self.get_account_with_fixed_root(new_address) {
|
|
||||||
datapoint_info!(datapoint_name, ("slot", self.slot, i64));
|
|
||||||
|
|
||||||
// Burn lamports in the old account
|
|
||||||
self.capitalization
|
|
||||||
.fetch_sub(old_account.lamports(), Relaxed);
|
|
||||||
|
|
||||||
// Transfer new account to old account
|
|
||||||
self.store_account(old_address, &new_account);
|
|
||||||
|
|
||||||
// Clear new account
|
|
||||||
self.store_account(new_address, &AccountSharedData::default());
|
|
||||||
|
|
||||||
// Unload a program from the bank's cache
|
|
||||||
self.loaded_programs_cache
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.remove_programs([*old_address].into_iter());
|
|
||||||
|
|
||||||
self.calculate_and_update_accounts_data_size_delta_off_chain(
|
|
||||||
old_account.data().len(),
|
|
||||||
new_account.data().len(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all the accounts for this bank and calculate stats
|
/// Get all the accounts for this bank and calculate stats
|
||||||
pub fn get_total_accounts_stats(&self) -> ScanResult<TotalAccountsStats> {
|
pub fn get_total_accounts_stats(&self) -> ScanResult<TotalAccountsStats> {
|
||||||
let accounts = self.get_all_accounts()?;
|
let accounts = self.get_all_accounts()?;
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
use {
|
||||||
|
super::Bank,
|
||||||
|
log::*,
|
||||||
|
solana_accounts_db::accounts_index::ZeroLamport,
|
||||||
|
solana_sdk::{
|
||||||
|
account::{Account, AccountSharedData, ReadableAccount},
|
||||||
|
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
|
||||||
|
pubkey::Pubkey,
|
||||||
|
},
|
||||||
|
std::sync::atomic::Ordering::Relaxed,
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Errors returned by `replace_account` methods
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ReplaceAccountError {
|
||||||
|
/// Account not found
|
||||||
|
#[error("Account not found: {0:?}")]
|
||||||
|
AccountNotFound(Pubkey),
|
||||||
|
/// Account exists
|
||||||
|
#[error("Account exists: {0:?}")]
|
||||||
|
AccountExists(Pubkey),
|
||||||
|
#[error("Bincode Error: {0}")]
|
||||||
|
BincodeError(#[from] bincode::Error),
|
||||||
|
/// Not an upgradeable program
|
||||||
|
#[error("Not an upgradeable program")]
|
||||||
|
NotAnUpgradeableProgram,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves one account in place of another
|
||||||
|
/// `source`: the account to replace with
|
||||||
|
/// `destination`: the account to be replaced
|
||||||
|
fn move_account<U, V>(
|
||||||
|
bank: &Bank,
|
||||||
|
source_address: &Pubkey,
|
||||||
|
source_account: &V,
|
||||||
|
destination_address: &Pubkey,
|
||||||
|
destination_account: Option<&U>,
|
||||||
|
) where
|
||||||
|
U: ReadableAccount + Sync + ZeroLamport,
|
||||||
|
V: ReadableAccount + Sync + ZeroLamport,
|
||||||
|
{
|
||||||
|
let (destination_lamports, destination_len) = match destination_account {
|
||||||
|
Some(destination_account) => (
|
||||||
|
destination_account.lamports(),
|
||||||
|
destination_account.data().len(),
|
||||||
|
),
|
||||||
|
None => (0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Burn lamports in the destination account
|
||||||
|
bank.capitalization.fetch_sub(destination_lamports, Relaxed);
|
||||||
|
|
||||||
|
// Transfer source account to destination account
|
||||||
|
bank.store_account(destination_address, source_account);
|
||||||
|
|
||||||
|
// Clear source account
|
||||||
|
bank.store_account(source_address, &AccountSharedData::default());
|
||||||
|
|
||||||
|
bank.calculate_and_update_accounts_data_size_delta_off_chain(
|
||||||
|
destination_len,
|
||||||
|
source_account.data().len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use to replace non-upgradeable programs by feature activation
|
||||||
|
/// `source`: the non-upgradeable program account to replace with
|
||||||
|
/// `destination`: the non-upgradeable program account to be replaced
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn replace_non_upgradeable_program_account(
|
||||||
|
bank: &Bank,
|
||||||
|
source_address: &Pubkey,
|
||||||
|
destination_address: &Pubkey,
|
||||||
|
datapoint_name: &'static str,
|
||||||
|
) -> Result<(), ReplaceAccountError> {
|
||||||
|
let destination_account = bank
|
||||||
|
.get_account_with_fixed_root(destination_address)
|
||||||
|
.ok_or(ReplaceAccountError::AccountNotFound(*destination_address))?;
|
||||||
|
let source_account = bank
|
||||||
|
.get_account_with_fixed_root(source_address)
|
||||||
|
.ok_or(ReplaceAccountError::AccountNotFound(*source_address))?;
|
||||||
|
|
||||||
|
datapoint_info!(datapoint_name, ("slot", bank.slot, i64));
|
||||||
|
|
||||||
|
move_account(
|
||||||
|
bank,
|
||||||
|
source_address,
|
||||||
|
&source_account,
|
||||||
|
destination_address,
|
||||||
|
Some(&destination_account),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unload a program from the bank's cache
|
||||||
|
bank.loaded_programs_cache
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.remove_programs([*destination_address].into_iter());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use to replace an empty account with a program by feature activation
|
||||||
|
/// Note: The upgradeable program should have both:
|
||||||
|
/// - Program account
|
||||||
|
/// - Program data account
|
||||||
|
/// `source`: the upgradeable program account to replace with
|
||||||
|
/// `destination`: the empty account to be replaced
|
||||||
|
pub(crate) fn replace_empty_account_with_upgradeable_program(
|
||||||
|
bank: &Bank,
|
||||||
|
source_address: &Pubkey,
|
||||||
|
destination_address: &Pubkey,
|
||||||
|
datapoint_name: &'static str,
|
||||||
|
) -> Result<(), ReplaceAccountError> {
|
||||||
|
// Must be attempting to replace an empty account with a program
|
||||||
|
// account _and_ data account
|
||||||
|
let source_account = bank
|
||||||
|
.get_account_with_fixed_root(source_address)
|
||||||
|
.ok_or(ReplaceAccountError::AccountNotFound(*source_address))?;
|
||||||
|
|
||||||
|
let (destination_data_address, _) = Pubkey::find_program_address(
|
||||||
|
&[destination_address.as_ref()],
|
||||||
|
&bpf_loader_upgradeable::id(),
|
||||||
|
);
|
||||||
|
let (source_data_address, _) =
|
||||||
|
Pubkey::find_program_address(&[source_address.as_ref()], &bpf_loader_upgradeable::id());
|
||||||
|
|
||||||
|
// Make sure the data within the source account is the PDA of its
|
||||||
|
// data account. This also means it has at least the necessary
|
||||||
|
// lamports for rent.
|
||||||
|
let source_state = bincode::deserialize::<UpgradeableLoaderState>(source_account.data())?;
|
||||||
|
if !matches!(source_state, UpgradeableLoaderState::Program { .. }) {
|
||||||
|
return Err(ReplaceAccountError::NotAnUpgradeableProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_data_account = bank
|
||||||
|
.get_account_with_fixed_root(&source_data_address)
|
||||||
|
.ok_or(ReplaceAccountError::AccountNotFound(source_data_address))?;
|
||||||
|
|
||||||
|
// Make sure the destination account is empty
|
||||||
|
// We aren't going to check that there isn't a data account at
|
||||||
|
// the known program-derived address (ie. `destination_data_address`),
|
||||||
|
// because if it exists, it will be overwritten
|
||||||
|
if bank
|
||||||
|
.get_account_with_fixed_root(destination_address)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(ReplaceAccountError::AccountExists(*destination_address));
|
||||||
|
}
|
||||||
|
let state = UpgradeableLoaderState::Program {
|
||||||
|
programdata_address: destination_data_address,
|
||||||
|
};
|
||||||
|
let data = bincode::serialize(&state)?;
|
||||||
|
let lamports = bank.get_minimum_balance_for_rent_exemption(data.len());
|
||||||
|
let created_program_account = Account {
|
||||||
|
lamports,
|
||||||
|
data,
|
||||||
|
owner: bpf_loader_upgradeable::id(),
|
||||||
|
executable: true,
|
||||||
|
rent_epoch: source_account.rent_epoch(),
|
||||||
|
};
|
||||||
|
|
||||||
|
datapoint_info!(datapoint_name, ("slot", bank.slot, i64));
|
||||||
|
let change_in_capitalization = source_account.lamports().saturating_sub(lamports);
|
||||||
|
|
||||||
|
// Replace the destination data account with the source one
|
||||||
|
// If the destination data account does not exist, it will be created
|
||||||
|
// If it does exist, it will be overwritten
|
||||||
|
move_account(
|
||||||
|
bank,
|
||||||
|
&source_data_address,
|
||||||
|
&source_data_account,
|
||||||
|
&destination_data_address,
|
||||||
|
bank.get_account_with_fixed_root(&destination_data_address)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the source data account's PDA into the destination program account
|
||||||
|
move_account(
|
||||||
|
bank,
|
||||||
|
source_address,
|
||||||
|
&created_program_account,
|
||||||
|
destination_address,
|
||||||
|
None::<&AccountSharedData>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Any remaining lamports in the source program account are burnt
|
||||||
|
bank.capitalization
|
||||||
|
.fetch_sub(change_in_capitalization, Relaxed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -8,6 +8,10 @@ use {
|
||||||
},
|
},
|
||||||
crate::{
|
crate::{
|
||||||
accounts_background_service::{PrunedBanksRequestHandler, SendDroppedBankCallback},
|
accounts_background_service::{PrunedBanksRequestHandler, SendDroppedBankCallback},
|
||||||
|
bank::replace_account::{
|
||||||
|
replace_empty_account_with_upgradeable_program,
|
||||||
|
replace_non_upgradeable_program_account, ReplaceAccountError,
|
||||||
|
},
|
||||||
bank_client::BankClient,
|
bank_client::BankClient,
|
||||||
epoch_rewards_hasher::hash_rewards_into_partitions,
|
epoch_rewards_hasher::hash_rewards_into_partitions,
|
||||||
genesis_utils::{
|
genesis_utils::{
|
||||||
|
@ -8003,42 +8007,403 @@ fn test_compute_active_feature_set() {
|
||||||
assert!(feature_set.is_active(&test_feature));
|
assert!(feature_set.is_active(&test_feature));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn test_program_replace_set_up_account<T: serde::Serialize>(
|
||||||
fn test_program_replacement() {
|
bank: &Bank,
|
||||||
let mut bank = create_simple_test_bank(0);
|
pubkey: &Pubkey,
|
||||||
|
lamports: u64,
|
||||||
// Setup original program account
|
state: &T,
|
||||||
let old_address = Pubkey::new_unique();
|
owner: &Pubkey,
|
||||||
let new_address = Pubkey::new_unique();
|
executable: bool,
|
||||||
bank.store_account_and_update_capitalization(
|
) -> AccountSharedData {
|
||||||
&old_address,
|
let data_len = bincode::serialized_size(state).unwrap() as usize;
|
||||||
&AccountSharedData::from(Account {
|
let mut account = AccountSharedData::from(Account {
|
||||||
lamports: 100,
|
lamports,
|
||||||
..Account::default()
|
owner: *owner,
|
||||||
}),
|
executable,
|
||||||
);
|
data: vec![0u8; data_len],
|
||||||
assert_eq!(bank.get_balance(&old_address), 100);
|
|
||||||
|
|
||||||
// Setup new program account
|
|
||||||
let new_program_account = AccountSharedData::from(Account {
|
|
||||||
lamports: 123,
|
|
||||||
..Account::default()
|
..Account::default()
|
||||||
});
|
});
|
||||||
bank.store_account_and_update_capitalization(&new_address, &new_program_account);
|
account.serialize_data(state).unwrap();
|
||||||
assert_eq!(bank.get_balance(&new_address), 123);
|
bank.store_account_and_update_capitalization(pubkey, &account);
|
||||||
|
assert_eq!(bank.get_balance(pubkey), lamports);
|
||||||
|
account
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replace_non_upgradeable_program_account() {
|
||||||
|
// Non-upgradeable program
|
||||||
|
// - Destination: [Destination program data]
|
||||||
|
// - Source: [*Source program data]
|
||||||
|
//
|
||||||
|
// Should replace the destination program account with the source program account:
|
||||||
|
// - Destination: [*Source program data]
|
||||||
|
let bpf_id = bpf_loader::id();
|
||||||
|
let bank = create_simple_test_bank(0);
|
||||||
|
|
||||||
|
let destination = Pubkey::new_unique();
|
||||||
|
let destination_state = vec![0u8; 4];
|
||||||
|
let destination_lamports = bank.get_minimum_balance_for_rent_exemption(destination_state.len());
|
||||||
|
test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&destination,
|
||||||
|
destination_lamports,
|
||||||
|
&destination_state,
|
||||||
|
&bpf_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let source = Pubkey::new_unique();
|
||||||
|
let source_state = vec![6; 30];
|
||||||
|
let source_lamports = bank.get_minimum_balance_for_rent_exemption(source_state.len());
|
||||||
|
let check_source_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
source_lamports,
|
||||||
|
&source_state,
|
||||||
|
&bpf_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let check_data_account_data = check_source_account.data().to_vec();
|
||||||
|
|
||||||
let original_capitalization = bank.capitalization();
|
let original_capitalization = bank.capitalization();
|
||||||
|
|
||||||
bank.replace_program_account(&old_address, &new_address, "bank-apply_program_replacement");
|
replace_non_upgradeable_program_account(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
&destination,
|
||||||
|
"bank-apply_program_replacement",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// New program account is now empty
|
// Destination program account balance is now the source program account's balance
|
||||||
assert_eq!(bank.get_balance(&new_address), 0);
|
assert_eq!(bank.get_balance(&destination), source_lamports);
|
||||||
|
|
||||||
// Old program account holds the new program account
|
// Source program account is now empty
|
||||||
assert_eq!(bank.get_account(&old_address), Some(new_program_account));
|
assert_eq!(bank.get_balance(&source), 0);
|
||||||
|
|
||||||
// Lamports in the old token account were burnt
|
// Destination program account now holds the source program data, ie:
|
||||||
assert_eq!(bank.capitalization(), original_capitalization - 100);
|
// - Destination: [*Source program data]
|
||||||
|
let destination_account = bank.get_account(&destination).unwrap();
|
||||||
|
assert_eq!(destination_account.data(), &check_data_account_data);
|
||||||
|
|
||||||
|
// Ownership & executable match the source program account
|
||||||
|
assert_eq!(destination_account.owner(), &bpf_id);
|
||||||
|
assert!(destination_account.executable());
|
||||||
|
|
||||||
|
// The destination account's original lamports balance was burnt
|
||||||
|
assert_eq!(
|
||||||
|
bank.capitalization(),
|
||||||
|
original_capitalization - destination_lamports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(
|
||||||
|
Pubkey::new_unique(),
|
||||||
|
None;
|
||||||
|
"Empty destination account _without_ corresponding data account"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
Pubkey::new_unique(),
|
||||||
|
Some(vec![4; 40]);
|
||||||
|
"Empty destination account _with_ corresponding data account"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
feature::id(), // `Feature11111111`
|
||||||
|
None;
|
||||||
|
"Native destination account _without_ corresponding data account"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
feature::id(), // `Feature11111111`
|
||||||
|
Some(vec![4; 40]);
|
||||||
|
"Native destination account _with_ corresponding data account"
|
||||||
|
)]
|
||||||
|
fn test_replace_empty_account_with_upgradeable_program_success(
|
||||||
|
destination: Pubkey,
|
||||||
|
maybe_destination_data_state: Option<Vec<u8>>, // Inner data of the destination program _data_ account
|
||||||
|
) {
|
||||||
|
// Ensures a program account and data account are created when replacing an
|
||||||
|
// empty account, ie:
|
||||||
|
// - Destination: PDA(DestinationData)
|
||||||
|
// - DestinationData: [Destination program data]
|
||||||
|
//
|
||||||
|
// If the destination data account exists, it will be overwritten
|
||||||
|
let bpf_upgradeable_id = bpf_loader_upgradeable::id();
|
||||||
|
let bank = create_simple_test_bank(0);
|
||||||
|
|
||||||
|
// Create the test source accounts, one for program and one for data
|
||||||
|
let source = Pubkey::new_unique();
|
||||||
|
let (source_data, _) = Pubkey::find_program_address(&[source.as_ref()], &bpf_upgradeable_id);
|
||||||
|
let source_state = UpgradeableLoaderState::Program {
|
||||||
|
programdata_address: source_data,
|
||||||
|
};
|
||||||
|
let source_lamports =
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program());
|
||||||
|
let source_data_state = vec![6; 30];
|
||||||
|
let source_data_lamports = bank.get_minimum_balance_for_rent_exemption(source_data_state.len());
|
||||||
|
test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
source_lamports,
|
||||||
|
&source_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let check_source_data_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source_data,
|
||||||
|
source_data_lamports,
|
||||||
|
&source_data_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let check_data_account_data = check_source_data_account.data().to_vec();
|
||||||
|
|
||||||
|
// Derive the well-known PDA address for the destination data account
|
||||||
|
let (destination_data, _) =
|
||||||
|
Pubkey::find_program_address(&[destination.as_ref()], &bpf_upgradeable_id);
|
||||||
|
|
||||||
|
// Determine the lamports that will be burnt after the replacement
|
||||||
|
let burnt_after_rent = if let Some(destination_data_state) = maybe_destination_data_state {
|
||||||
|
// Create the data account if necessary
|
||||||
|
let destination_data_lamports =
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(destination_data_state.len());
|
||||||
|
test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&destination_data,
|
||||||
|
destination_data_lamports,
|
||||||
|
&destination_data_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
destination_data_lamports + source_lamports
|
||||||
|
- bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())
|
||||||
|
} else {
|
||||||
|
source_lamports
|
||||||
|
- bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_capitalization = bank.capitalization();
|
||||||
|
|
||||||
|
// Do the replacement
|
||||||
|
replace_empty_account_with_upgradeable_program(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
&destination,
|
||||||
|
"bank-apply_empty_account_replacement_for_program",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Destination program account was created and funded to pay for minimum rent
|
||||||
|
// for the PDA
|
||||||
|
assert_eq!(
|
||||||
|
bank.get_balance(&destination),
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Destination data account was created, now holds the source data account's balance
|
||||||
|
assert_eq!(bank.get_balance(&destination_data), source_data_lamports);
|
||||||
|
|
||||||
|
// Source program accounts are now empty
|
||||||
|
assert_eq!(bank.get_balance(&source), 0);
|
||||||
|
assert_eq!(bank.get_balance(&source_data), 0);
|
||||||
|
|
||||||
|
// Destination program account holds the PDA, ie:
|
||||||
|
// - Destination: PDA(DestinationData)
|
||||||
|
let destination_account = bank.get_account(&destination).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
destination_account.data(),
|
||||||
|
&bincode::serialize(&UpgradeableLoaderState::Program {
|
||||||
|
programdata_address: destination_data
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Destination data account holds the source data, ie:
|
||||||
|
// - DestinationData: [*Source program data]
|
||||||
|
let destination_data_account = bank.get_account(&destination_data).unwrap();
|
||||||
|
assert_eq!(destination_data_account.data(), &check_data_account_data);
|
||||||
|
|
||||||
|
// Ownership & executable match the source program accounts
|
||||||
|
assert_eq!(destination_account.owner(), &bpf_upgradeable_id);
|
||||||
|
assert!(destination_account.executable());
|
||||||
|
assert_eq!(destination_data_account.owner(), &bpf_upgradeable_id);
|
||||||
|
assert!(!destination_data_account.executable());
|
||||||
|
|
||||||
|
// The remaining lamports from both program accounts minus the rent-exempt
|
||||||
|
// minimum were burnt
|
||||||
|
assert_eq!(
|
||||||
|
bank.capitalization(),
|
||||||
|
original_capitalization - burnt_after_rent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(
|
||||||
|
None;
|
||||||
|
"Existing destination account _without_ corresponding data account"
|
||||||
|
)]
|
||||||
|
#[test_case(
|
||||||
|
Some(vec![4; 40]);
|
||||||
|
"Existing destination account _with_ corresponding data account"
|
||||||
|
)]
|
||||||
|
fn test_replace_empty_account_with_upgradeable_program_fail_when_account_exists(
|
||||||
|
maybe_destination_data_state: Option<Vec<u8>>, // Inner data of the destination program _data_ account
|
||||||
|
) {
|
||||||
|
// Should not be allowed to execute replacement
|
||||||
|
let bpf_upgradeable_id = bpf_loader_upgradeable::id();
|
||||||
|
let bank = create_simple_test_bank(0);
|
||||||
|
|
||||||
|
// Create the test destination account with some arbitrary data and lamports balance
|
||||||
|
let destination = Pubkey::new_unique();
|
||||||
|
let destination_state = vec![0, 0, 0, 0]; // Arbitrary bytes, doesn't matter
|
||||||
|
let destination_lamports = bank.get_minimum_balance_for_rent_exemption(destination_state.len());
|
||||||
|
let destination_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&destination,
|
||||||
|
destination_lamports,
|
||||||
|
&destination_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the test source accounts, one for program and one for data
|
||||||
|
let source = Pubkey::new_unique();
|
||||||
|
let (source_data, _) = Pubkey::find_program_address(&[source.as_ref()], &bpf_upgradeable_id);
|
||||||
|
let source_state = UpgradeableLoaderState::Program {
|
||||||
|
programdata_address: source_data,
|
||||||
|
};
|
||||||
|
let source_lamports =
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program());
|
||||||
|
let source_data_state = vec![6; 30];
|
||||||
|
let source_data_lamports = bank.get_minimum_balance_for_rent_exemption(source_data_state.len());
|
||||||
|
let source_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
source_lamports,
|
||||||
|
&source_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let source_data_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source_data,
|
||||||
|
source_data_lamports,
|
||||||
|
&source_data_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Derive the well-known PDA address for the destination data account
|
||||||
|
let (destination_data, _) =
|
||||||
|
Pubkey::find_program_address(&[destination.as_ref()], &bpf_upgradeable_id);
|
||||||
|
|
||||||
|
// Create the data account if necessary
|
||||||
|
let destination_data_account =
|
||||||
|
if let Some(destination_data_state) = maybe_destination_data_state {
|
||||||
|
let destination_data_lamports =
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(destination_data_state.len());
|
||||||
|
let destination_data_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&destination_data,
|
||||||
|
destination_data_lamports,
|
||||||
|
&destination_data_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
Some(destination_data_account)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_capitalization = bank.capitalization();
|
||||||
|
|
||||||
|
// Attempt the replacement
|
||||||
|
assert_matches!(
|
||||||
|
replace_empty_account_with_upgradeable_program(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
&destination,
|
||||||
|
"bank-apply_empty_account_replacement_for_program",
|
||||||
|
)
|
||||||
|
.unwrap_err(),
|
||||||
|
ReplaceAccountError::AccountExists(..)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Everything should be unchanged
|
||||||
|
assert_eq!(bank.get_account(&destination).unwrap(), destination_account);
|
||||||
|
if let Some(destination_data_account) = destination_data_account {
|
||||||
|
assert_eq!(
|
||||||
|
bank.get_account(&destination_data).unwrap(),
|
||||||
|
destination_data_account
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(bank.get_account(&source).unwrap(), source_account);
|
||||||
|
assert_eq!(bank.get_account(&source_data).unwrap(), source_data_account);
|
||||||
|
assert_eq!(bank.capitalization(), original_capitalization);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replace_empty_account_with_upgradeable_program_fail_when_not_upgradeable_program() {
|
||||||
|
// Should not be allowed to execute replacement
|
||||||
|
let bpf_upgradeable_id = bpf_loader_upgradeable::id();
|
||||||
|
let bank = create_simple_test_bank(0);
|
||||||
|
|
||||||
|
// Create the test destination account with some arbitrary data and lamports balance
|
||||||
|
let destination = Pubkey::new_unique();
|
||||||
|
let destination_state = vec![0, 0, 0, 0]; // Arbitrary bytes, doesn't matter
|
||||||
|
let destination_lamports = bank.get_minimum_balance_for_rent_exemption(destination_state.len());
|
||||||
|
let destination_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&destination,
|
||||||
|
destination_lamports,
|
||||||
|
&destination_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the test source accounts, one for program and one for data
|
||||||
|
let source = Pubkey::new_unique();
|
||||||
|
let (source_data, _) = Pubkey::find_program_address(&[source.as_ref()], &bpf_upgradeable_id);
|
||||||
|
let source_state = [0, 0, 0, 0]; // Arbitrary bytes, NOT an upgradeable program
|
||||||
|
let source_lamports =
|
||||||
|
bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program());
|
||||||
|
let source_data_state = vec![6; 30];
|
||||||
|
let source_data_lamports = bank.get_minimum_balance_for_rent_exemption(source_data_state.len());
|
||||||
|
let source_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
source_lamports,
|
||||||
|
&source_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let source_data_account = test_program_replace_set_up_account(
|
||||||
|
&bank,
|
||||||
|
&source_data,
|
||||||
|
source_data_lamports,
|
||||||
|
&source_data_state,
|
||||||
|
&bpf_upgradeable_id,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let original_capitalization = bank.capitalization();
|
||||||
|
|
||||||
|
// Attempt the replacement
|
||||||
|
assert_matches!(
|
||||||
|
replace_empty_account_with_upgradeable_program(
|
||||||
|
&bank,
|
||||||
|
&source,
|
||||||
|
&destination,
|
||||||
|
"bank-apply_empty_account_replacement_for_program",
|
||||||
|
)
|
||||||
|
.unwrap_err(),
|
||||||
|
ReplaceAccountError::NotAnUpgradeableProgram
|
||||||
|
);
|
||||||
|
|
||||||
|
// Everything should be unchanged
|
||||||
|
assert_eq!(bank.get_account(&destination).unwrap(), destination_account);
|
||||||
|
assert_eq!(bank.get_account(&source).unwrap(), source_account);
|
||||||
|
assert_eq!(bank.get_account(&source_data).unwrap(), source_data_account);
|
||||||
|
assert_eq!(bank.capitalization(), original_capitalization);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn min_rent_exempt_balance_for_sysvars(bank: &Bank, sysvar_ids: &[Pubkey]) -> u64 {
|
fn min_rent_exempt_balance_for_sysvars(bank: &Bank, sysvar_ids: &[Pubkey]) -> u64 {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
//! Contains replacement program IDs for the feature gate program
|
||||||
|
|
||||||
|
pub(crate) mod noop_program {
|
||||||
|
solana_sdk::declare_id!("2rqZsQBbacRbuAuTSuJ7n49UQT9fzes8RLggFcmB9YuN");
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ pub mod commitment;
|
||||||
mod epoch_rewards_hasher;
|
mod epoch_rewards_hasher;
|
||||||
pub mod epoch_stakes;
|
pub mod epoch_stakes;
|
||||||
pub mod genesis_utils;
|
pub mod genesis_utils;
|
||||||
|
pub mod inline_feature_gate_program;
|
||||||
pub mod inline_spl_associated_token_account;
|
pub mod inline_spl_associated_token_account;
|
||||||
pub mod loader_utils;
|
pub mod loader_utils;
|
||||||
pub mod non_circulating_supply;
|
pub mod non_circulating_supply;
|
||||||
|
|
|
@ -700,6 +700,10 @@ pub mod better_error_codes_for_tx_lamport_check {
|
||||||
solana_sdk::declare_id!("Ffswd3egL3tccB6Rv3XY6oqfdzn913vUcjCSnpvCKpfx");
|
solana_sdk::declare_id!("Ffswd3egL3tccB6Rv3XY6oqfdzn913vUcjCSnpvCKpfx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod programify_feature_gate_program {
|
||||||
|
solana_sdk::declare_id!("8GdovDzVwWU5edz2G697bbB7GZjrUc6aQZLWyNNAtHdg");
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Map of feature identifiers to user-visible description
|
/// Map of feature identifiers to user-visible description
|
||||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||||
|
@ -870,6 +874,7 @@ lazy_static! {
|
||||||
(require_rent_exempt_split_destination::id(), "Require stake split destination account to be rent exempt"),
|
(require_rent_exempt_split_destination::id(), "Require stake split destination account to be rent exempt"),
|
||||||
(better_error_codes_for_tx_lamport_check::id(), "better error codes for tx lamport check #33353"),
|
(better_error_codes_for_tx_lamport_check::id(), "better error codes for tx lamport check #33353"),
|
||||||
(enable_alt_bn128_compression_syscall::id(), "add alt_bn128 compression syscalls"),
|
(enable_alt_bn128_compression_syscall::id(), "add alt_bn128 compression syscalls"),
|
||||||
|
(programify_feature_gate_program::id(), "move feature gate activation logic to an on-chain program #32783"),
|
||||||
/*************** ADD NEW FEATURES HERE ***************/
|
/*************** ADD NEW FEATURES HERE ***************/
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
|
|
Loading…
Reference in New Issue