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 upgrade_core_bpf_program module #2

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ mod sysvar_cache;
#[cfg(test)]
pub(crate) mod tests;
mod transaction_account_state_info;
mod upgrade_core_bpf_program;

pub const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 60.0 * 60.0;

Expand Down
142 changes: 142 additions & 0 deletions runtime/src/bank/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use {
},
crate::{
accounts_background_service::{PrunedBanksRequestHandler, SendDroppedBankCallback},
bank::upgrade_core_bpf_program::{
migrate_native_program_to_core_bpf, upgrade_core_bpf_program, NativeProgram,
UpgradeCoreBpfProgramError,
},
bank_client::BankClient,
bank_forks::BankForks,
epoch_rewards_hasher::hash_rewards_into_partitions,
Expand Down Expand Up @@ -7827,6 +7831,144 @@ fn min_rent_exempt_balance_for_sysvars(bank: &Bank, sysvar_ids: &[Pubkey]) -> u6
.sum()
}

fn test_core_bpf_set_up_account<T: serde::Serialize>(
bank: &Bank,
pubkey: &Pubkey,
executable: bool,
owner: &Pubkey,
state: &T,
) -> AccountSharedData {
let data_len = bincode::serialized_size(state).unwrap() as usize;
let lamports = bank.get_minimum_balance_for_rent_exemption(data_len);
let mut account = AccountSharedData::from(Account {
lamports,
owner: *owner,
executable,
data: vec![0u8; data_len],
..Account::default()
});
account.serialize_data(state).unwrap();
bank.store_account_and_update_capitalization(pubkey, &account);
assert_eq!(bank.get_balance(pubkey), lamports);
account
}

#[test_case(NativeProgram::AddressLookupTable)]
#[test_case(NativeProgram::BpfLoader)]
#[test_case(NativeProgram::BpfLoaderUpgradeable)]
#[test_case(NativeProgram::ComputeBudget)]
#[test_case(NativeProgram::Config)]
#[test_case(NativeProgram::Ed25519)]
#[test_case(NativeProgram::FeatureGate)]
#[test_case(NativeProgram::NativeLoader)]
#[test_case(NativeProgram::Secp256k1)]
#[test_case(NativeProgram::System)]
#[test_case(NativeProgram::Stake)]
#[test_case(NativeProgram::Vote)]
fn test_migrate_native_program(target: NativeProgram) {
let bank = create_simple_test_bank(0);
let expected_capitalization =
bank.capitalization() - bank.get_account(&target.id()).unwrap().lamports();

let source_address = Pubkey::new_unique();
let example_program_state = vec![0u8; 4];

// Fail account not found
assert_eq!(
migrate_native_program_to_core_bpf(&bank, target, &source_address, "").unwrap_err(),
Err(UpgradeCoreBpfProgramError::AccountNotFound(source_address))
);

// Fail wrong owner
test_core_bpf_set_up_account(
&bank,
&source_address,
true,
&bpf_loader_upgradeable::id(),
example_program_state,
);
assert_eq!(
migrate_native_program_to_core_bpf(&bank, target, &source_address, "").unwrap_err(),
Err(UpgradeCoreBpfProgramError::IncorrectOwner(source_address))
);

// Fail account not executable
test_core_bpf_set_up_account(
&bank,
&source_address,
false,
&bpf_loader::id(),
example_program_state,
);
assert_eq!(
migrate_native_program_to_core_bpf(&bank, target, &source_address, "").unwrap_err(),
Err(UpgradeCoreBpfProgramError::AccountNotExecutable(
source_address
))
);

// Fail program has data account
let source_data_address =
Pubkey::find_program_address(&[source_address.as_ref()], &bpf_loader_upgradeable::id()).0;
test_core_bpf_set_up_account(
&bank,
&source_address,
true,
&bpf_loader::id(),
&UpgradeableLoaderState::Program {
programdata_address: source_data_address,
},
);
test_core_bpf_set_up_account(
&bank,
&source_data_address,
true,
&bpf_loader::id(),
example_program_state,
);
assert_eq!(
migrate_native_program_to_core_bpf(&bank, target, &source_address, "").unwrap_err(),
Err(UpgradeCoreBpfProgramError::ProgramHasDataAccount(
source_address
))
);

// Success
let pre_migration_source_account = test_core_bpf_set_up_account(
&bank,
&source_address,
true,
&bpf_loader::id(),
example_program_state,
);
migrate_native_program_to_core_bpf(&bank, target, &source_address, "").unwrap();

// Assert the target account is the same as the original source account
let post_migration_target_account = bank.get_account(&target.id()).unwrap();
assert_eq!(pre_migration_source_account, post_migration_target_account);
// Assert the source account was deleted
let post_migration_source_account = bank.get_account(&source_address);
assert!(post_migration_source_account.is_none());
// Assert the lamports of the target account were burnt
assert_eq!(bank.capitalization(), expected_capitalization);
}

#[test_case(NativeProgram::AddressLookupTable)]
#[test_case(NativeProgram::BpfLoader)]
#[test_case(NativeProgram::BpfLoaderUpgradeable)]
#[test_case(NativeProgram::ComputeBudget)]
#[test_case(NativeProgram::Config)]
#[test_case(NativeProgram::Ed25519)]
#[test_case(NativeProgram::FeatureGate)]
#[test_case(NativeProgram::NativeLoader)]
#[test_case(NativeProgram::Secp256k1)]
#[test_case(NativeProgram::System)]
#[test_case(NativeProgram::Stake)]
#[test_case(NativeProgram::Vote)]
fn test_upgrade_core_bpf_program(target: NativeProgram) {
todo!()
}

#[test]
fn test_adjust_sysvar_balance_for_rent() {
let bank = create_simple_test_bank(0);
Expand Down
197 changes: 197 additions & 0 deletions runtime/src/bank/upgrade_core_bpf_program.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use {
super::Bank,
solana_sdk::{
account::{Account, AccountSharedData},
bpf_loader::ID as BPF_LOADER_ID,
bpf_loader_upgradeable::ID as BPF_LOADER_UPGRADEABLE_ID,
native_loader::ID as NATIVE_LOADER_ID,
pubkey::Pubkey,
},
std::sync::atomic::Ordering::Relaxed,
thiserror::Error,
};

/// Errors returned by `replace_account` methods
#[derive(Debug, Error)]
pub enum UpgradeCoreBpfProgramError {
/// Account not executable
#[error("Account not executable: {0:?}")]
AccountNotExecutable(Pubkey),
/// Account not found
#[error("Account not found: {0:?}")]
AccountNotFound(Pubkey),
/// Incorrect account owner
#[error("Incorrect account owner for {0:?}")]
IncorrectOwner(Pubkey),
/// Program has a data account
#[error("Data account exists for {0:?}")]
ProgramHasDataAccount(Pubkey),
}

// Note: This enum is off the hot path until a program migration/upgrade is
// due.
#[allow(dead_code)]
pub(crate) enum NativeProgram {
AddressLookupTable,
BpfLoader,
BpfLoaderUpgradeable,
ComputeBudget,
Config,
Ed25519,
FeatureGate,
NativeLoader,
Secp256k1,
System,
Stake,
Vote,
// ZkTokenProof,
}

impl NativeProgram {
pub(crate) fn id(&self) -> Pubkey {
match self {
NativeProgram::AddressLookupTable => solana_sdk::address_lookup_table::program::id(),
NativeProgram::BpfLoader => solana_sdk::bpf_loader::id(),
NativeProgram::BpfLoaderUpgradeable => solana_sdk::bpf_loader_upgradeable::id(),
NativeProgram::ComputeBudget => solana_sdk::compute_budget::id(),
NativeProgram::Config => solana_sdk::config::program::id(),
NativeProgram::Ed25519 => solana_sdk::ed25519_program::id(),
NativeProgram::FeatureGate => solana_sdk::feature::id(),
NativeProgram::NativeLoader => solana_sdk::native_loader::id(),
NativeProgram::Secp256k1 => solana_sdk::secp256k1_program::id(),
NativeProgram::System => solana_sdk::system_program::id(),
NativeProgram::Stake => solana_sdk::stake::program::id(),
NativeProgram::Vote => solana_sdk::vote::program::id(),
// NativeProgram::ZkTokenProof => solana_zk_token_proof_program::id(),
}
}
}

struct UpgradeConfig {
program_address: Pubkey,
program_account: Account,
}

fn get_program_data_address(program_id: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID).0
}

/// Run checks on a core BPF or native program before performing a migration
/// or upgrade.
///
/// In either case, the program should:
/// * Exist
/// * Consist of only a program account, no program data account
/// * Be owned by the proper account:
/// * BPF programs should be owned by the non-upgradeable loader
/// * Native programs should be owned by the native loader
fn check_program(
bank: &Bank,
address: &Pubkey,
owner: &Pubkey,
) -> Result<UpgradeConfig, UpgradeCoreBpfProgramError> {
let program_address = *address;
// The program account should exist
let program_account: Account = bank
.get_account_with_fixed_root(&program_address)
.ok_or(UpgradeCoreBpfProgramError::AccountNotFound(program_address))?
.into();
// The program account should be owned by the specified program
if program_account.owner != *owner {
return Err(UpgradeCoreBpfProgramError::IncorrectOwner(program_address));
}
// The program should be executable
if !program_account.executable {
return Err(UpgradeCoreBpfProgramError::AccountNotExecutable(
program_address,
));
}
// The program data account should _not_ exist
let program_data_address = get_program_data_address(&program_address);
if bank
.get_account_with_fixed_root(&program_data_address)
.is_some()
{
return Err(UpgradeCoreBpfProgramError::ProgramHasDataAccount(
program_address,
));
}
Ok(UpgradeConfig {
program_address,
program_account,
})
}

fn move_program(bank: &Bank, source: UpgradeConfig, target: UpgradeConfig) {
// Burn lamports in the target program account
bank.capitalization
.fetch_sub(target.program_account.lamports, Relaxed);
// Transfer source program account to target program account, clear the
// source data account, and update the accounts data size delta
let (old_data_size, new_data_size) = (
target.program_account.data.len(),
source.program_account.data.len(),
);
bank.store_account(&target.program_address, &source.program_account);
bank.store_account(&source.program_address, &AccountSharedData::default());
bank.calculate_and_update_accounts_data_size_delta_off_chain(old_data_size, new_data_size);
// Unload the programs from the bank's cache
bank.loaded_programs_cache
.write()
.unwrap()
.remove_programs([source.program_address, target.program_address].into_iter());
}

/// Migrate a native program to BPF using a BPF version of the program,
/// deployed at some arbitrary address.
///
/// This function will move the deployed BPF program in place of the native
/// program by replacing the account at the native program's address with the
/// deployed BPF program's account.
///
/// This function performs a complete overwrite of the account, including the
/// owner program. The strict requirements for this swap can be found in each
/// "check" function.
// Note: This function is off the hot path until a program migration is due.
#[allow(dead_code)]
pub(crate) fn migrate_native_program_to_core_bpf(
bank: &Bank,
target: NativeProgram,
source_address: &Pubkey,
datapoint_name: &'static str,
) -> Result<(), UpgradeCoreBpfProgramError> {
// Source should be a BPF program owned by the non-upgradeable loader
// Target should be a native program owned by the native loader
let source = check_program(bank, source_address, &BPF_LOADER_ID)?;
let target = check_program(bank, &target.id(), &NATIVE_LOADER_ID)?;
datapoint_info!(datapoint_name, ("slot", bank.slot, i64));
move_program(bank, source, target);
Ok(())
}

/// Upgrade a core BPF program using a modified version of the program,
/// deployed at some arbitrary address.
///
/// This function will move the modified BPF program in place of the existing
/// program by replacing the account at the existing program's address with the
/// modified program's account.
///
/// This function performs a complete overwrite of the account, including the
/// owner program. The strict requirements for this swap can be found in each
/// "check" function.
// Note: This function is off the hot path until a program upgrade is due.
#[allow(dead_code)]
pub(crate) fn upgrade_core_bpf_program(
bank: &Bank,
target: NativeProgram,
Copy link
Owner Author

Choose a reason for hiding this comment

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

Because of this parameter, it might make sense to rename the enum from NativeProgram to something like CoreProgram. Or we can use a type alias, ie: type NativeProgram = CoreProgram;.

source_address: &Pubkey,
datapoint_name: &'static str,
) -> Result<(), UpgradeCoreBpfProgramError> {
// Source should be a BPF program owned by the non-upgradeable loader
// Target should be a BPF program owned by the non-upgradeable loader
let source = check_program(bank, source_address, &BPF_LOADER_ID)?;
let target = check_program(bank, &target.id(), &BPF_LOADER_ID)?;
datapoint_info!(datapoint_name, ("slot", bank.slot, i64));
move_program(bank, source, target);
Ok(())
}