diff --git a/Cargo.lock b/Cargo.lock index d14ea45e90d..05469cd497e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7260,10 +7260,12 @@ dependencies = [ name = "solana-bpf-loader-program-tests" version = "4.1.0-alpha.0" dependencies = [ + "agave-feature-set", "assert_matches", "bincode", "solana-account 4.2.0", "solana-bpf-loader-program", + "solana-clock", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -8861,17 +8863,17 @@ dependencies = [ [[package]] name = "solana-loader-v3-interface" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" dependencies = [ "serde", "serde_bytes", "serde_derive", "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-sdk-ids", - "solana-system-interface 2.0.0", + "solana-system-interface 3.2.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 25d1ec1ce08..2142adb37d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,7 +430,7 @@ solana-lattice-hash = { path = "lattice-hash", version = "=4.1.0-alpha.0", featu solana-leader-schedule = { path = "leader-schedule", version = "=4.1.0-alpha.0", features = ["agave-unstable-api"] } solana-ledger = { path = "ledger", version = "=4.1.0-alpha.0", features = ["agave-unstable-api"] } solana-loader-v2-interface = "3.0.0" -solana-loader-v3-interface = "6.1.0" +solana-loader-v3-interface = "6.1.1" solana-loader-v4-interface = "3.1.0" solana-loader-v4-program = { path = "programs/loader-v4", version = "=4.1.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-local-cluster = { path = "local-cluster", version = "=4.1.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/cli/src/program.rs b/cli/src/program.rs index dd284d9d9aa..cd2942cb3d0 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -43,7 +43,8 @@ use { solana_instruction::{Instruction, error::InstructionError}, solana_keypair::{Keypair, keypair_from_seed, read_keypair_file}, solana_loader_v3_interface::{ - get_program_data_address, instruction as loader_v3_instruction, + get_program_data_address, + instruction::{self as loader_v3_instruction, MINIMUM_EXTEND_PROGRAM_BYTES}, state::UpgradeableLoaderState, }, solana_message::Message, @@ -2452,6 +2453,35 @@ async fn process_extend_program( .ok_or_else(|| format!("Program {program_pubkey} is not upgradeable"))?; let blockhash = rpc_client.get_latest_blockhash().await?; + let feature_set = fetch_feature_set(rpc_client).await?; + let feature_snapshot = feature_set.snapshot(); + + if feature_snapshot.loader_v3_minimum_extend_program_size { + // SIMD-0431: Minimum Extend Program Size + // + // All extensions must be >= 10 KiB in additional_bytes, unless + // MAX_PERMITTED_DATA_LENGTH - current_len < 10 KiB. In that case, + // additional_bytes must be equal to the remaining free space. + let current_len = programdata_account.data.len(); + let headroom = (MAX_PERMITTED_DATA_LENGTH as usize).saturating_sub(current_len); + if additional_bytes < MINIMUM_EXTEND_PROGRAM_BYTES + && (additional_bytes as usize) != headroom + { + let err_msg = if (headroom as u32) < MINIMUM_EXTEND_PROGRAM_BYTES { + format!( + "Program is {headroom} bytes from maximum size, but {additional_bytes} were \ + requested. Please re-run the command with {headroom} additional bytes." + ) + } else { + format!( + "ExtendProgram requires a minimum of {MINIMUM_EXTEND_PROGRAM_BYTES} \ + additional bytes or to extend to maximum size, but only {additional_bytes} \ + were requested" + ) + }; + return Err(err_msg.into()); + } + } let instruction = loader_v3_instruction::extend_program( &program_pubkey, @@ -2951,9 +2981,21 @@ async fn extend_program_data_if_needed( return Ok(()); } - let additional_bytes = + let mut additional_bytes = u32::try_from(additional_bytes).expect("`u32` is big enough to hold an account size"); + let feature_set = fetch_feature_set(rpc_client).await?; + let feature_snapshot = feature_set.snapshot(); + + if feature_snapshot.loader_v3_minimum_extend_program_size { + // SIMD-0431: Have to bump `additional_bytes` to satisfy either the + // minimum size requirement or the remaining headroom to + // MAX_PERMITTED_DATA_SIZE. + let headroom = + u32::try_from(max_permitted_data_length.saturating_sub(current_len)).unwrap(); + additional_bytes = additional_bytes.max(MINIMUM_EXTEND_PROGRAM_BYTES.min(headroom)); + } + let instruction = loader_v3_instruction::extend_program(program_id, Some(fee_payer), additional_bytes); initial_instructions.push(instruction); diff --git a/cli/tests/program.rs b/cli/tests/program.rs index b2764685b2c..620b559ffd1 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -1,7 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] use { - agave_feature_set::{enable_alt_bn128_syscall, enable_extend_program_checked}, + agave_feature_set::{enable_alt_bn128_syscall, loader_v3_minimum_extend_program_size}, assert_matches::assert_matches, serde_json::Value, solana_account::{ReadableAccount, state_traits::StateMut}, @@ -19,9 +19,11 @@ use { solana_fee_calculator::FeeRateGovernor, solana_keypair::Keypair, solana_loader_v3_interface::{ - instruction as loader_v3_instruction, state::UpgradeableLoaderState, + instruction::{self as loader_v3_instruction, MINIMUM_EXTEND_PROGRAM_BYTES}, + state::UpgradeableLoaderState, }, solana_message::Message, + solana_native_token::LAMPORTS_PER_SOL, solana_net_utils::SocketAddrSpace, solana_pubkey::Pubkey, solana_rent::Rent, @@ -34,8 +36,8 @@ use { solana_sdk_ids::{bpf_loader_upgradeable, compute_budget}, solana_signature::Signature, solana_signer::{Signer, null_signer::NullSigner}, - solana_system_interface::program as system_program, - solana_test_validator::TestValidatorGenesis, + solana_system_interface::{MAX_PERMITTED_DATA_LENGTH, program as system_program}, + solana_test_validator::{TestValidator, TestValidatorGenesis}, solana_transaction::Transaction, solana_transaction_status::UiTransactionEncoding, std::{ @@ -49,7 +51,14 @@ use { test_case::test_case, }; -fn test_validator_genesis(mint_keypair: &Keypair) -> TestValidatorGenesis { +pub struct LoaderV3Features { + pub minimum_extend_program_size: bool, +} + +fn test_validator_genesis( + mint_keypair: &Keypair, + features: LoaderV3Features, +) -> TestValidatorGenesis { let mut genesis = TestValidatorGenesis::default(); genesis .fee_rate_governor(FeeRateGovernor::new(0, 0)) @@ -59,8 +68,15 @@ fn test_validator_genesis(mint_keypair: &Keypair) -> TestValidatorGenesis { }) .faucet_addr(Some(run_local_faucet_with_unique_port_for_tests( mint_keypair.insecure_clone(), - ))) - .deactivate_features(&[enable_extend_program_checked::id()]); + ))); + + let LoaderV3Features { + minimum_extend_program_size, + } = features; + if !minimum_extend_program_size { + genesis.deactivate_features(&[loader_v3_minimum_extend_program_size::id()]); + } + genesis } @@ -104,6 +120,83 @@ async fn expect_account_absent(rpc_client: &RpcClient, pubkey: Pubkey, absent_be ); } +struct ExtendProgramTestSetup<'a> { + test_validator: TestValidator, + config: CliConfig<'a>, + rpc_client: Arc, + programdata_pubkey: Pubkey, + program_len: usize, +} + +async fn setup_extend_program_test<'a>( + mint_keypair: &Keypair, + payer: &'a Keypair, + upgrade_authority: &'a Keypair, + program_keypair: &'a Keypair, + program_path: &Path, + features: LoaderV3Features, +) -> ExtendProgramTestSetup<'a> { + let test_validator = test_validator_genesis(mint_keypair, features) + .start_async_with_mint_address(mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + let rpc_client = setup_rpc_client(&mut config); + config.send_transaction_config = RpcSendTransactionConfig { + skip_preflight: false, + preflight_commitment: Some(CommitmentConfig::processed().commitment), + ..RpcSendTransactionConfig::default() + }; + config.output_format = OutputFormat::JsonCompact; + + let mut file = File::open(program_path).unwrap(); + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data).unwrap(); + let program_len = program_data.len(); + + config.signers = vec![payer]; + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: LAMPORTS_PER_SOL * 100, + }; + process_command(&config).await.unwrap(); + + config.signers = vec![payer, upgrade_authority, program_keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: Some(program_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, + program_signer_index: Some(2), + program_pubkey: Some(program_keypair.pubkey()), + buffer_signer_index: None, + buffer_pubkey: None, + upgrade_authority_signer_index: 1, + is_final: false, + max_len: None, + skip_fee_check: false, + compute_unit_price: None, + max_sign_attempts: 5, + auto_extend: false, + use_rpc: false, + skip_feature_verification: true, + }); + process_command(&config).await.unwrap(); + + let (programdata_pubkey, _) = Pubkey::find_program_address( + &[program_keypair.pubkey().as_ref()], + &bpf_loader_upgradeable::id(), + ); + + ExtendProgramTestSetup { + test_validator, + config, + rpc_client, + programdata_pubkey, + program_len, + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_cli_program_deploy_non_upgradeable() { agave_logger::setup(); @@ -115,10 +208,15 @@ async fn test_cli_program_deploy_non_upgradeable() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -326,10 +424,15 @@ async fn test_cli_program_deploy_no_authority() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -432,7 +535,12 @@ async fn test_cli_program_deploy_feature(enable_feature: bool, skip_preflight: b program_path.set_extension("so"); let mint_keypair = Keypair::new(); - let mut test_validator_builder = test_validator_genesis(&mint_keypair); + let mut test_validator_builder = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ); // Deactivate the enable alt bn128 syscall and try to submit a program with that syscall if !enable_feature { @@ -565,7 +673,12 @@ async fn test_cli_program_upgrade_with_feature(enable_feature: bool) { syscall_program_path.set_extension("so"); let mint_keypair = Keypair::new(); - let mut test_validator_builder = test_validator_genesis(&mint_keypair); + let mut test_validator_builder = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ); // Deactivate the enable alt bn128 syscall and try to submit a program with that syscall if !enable_feature { @@ -729,10 +842,15 @@ async fn test_cli_program_deploy_with_authority() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -1137,10 +1255,15 @@ async fn test_cli_program_upgrade_auto_extend(skip_preflight: bool) { noop_large_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -1310,10 +1433,15 @@ async fn test_cli_program_close_program() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -1435,74 +1563,26 @@ async fn test_cli_program_extend_program() { noop_large_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); - - let mut config = CliConfig::recent_for_tests(); - config.json_rpc_url = test_validator.rpc_url(); - let rpc_client = setup_rpc_client(&mut config); - - let mut file = File::open(noop_path.to_str().unwrap()).unwrap(); - let mut program_data = Vec::new(); - file.read_to_end(&mut program_data).unwrap(); - let max_len = program_data.len(); - let minimum_balance_for_programdata = rpc_client - .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata( - max_len, - )) - .await - .unwrap(); - let minimum_balance_for_program = rpc_client - .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()) - .await - .unwrap(); - let upgrade_authority = Keypair::new(); - let keypair = Keypair::new(); - let fee_headroom = 1_000_000; - config.signers = vec![&keypair]; - config.command = CliCommand::Airdrop { - pubkey: None, - lamports: 100 * minimum_balance_for_programdata - + minimum_balance_for_program - + fee_headroom, - }; - config.send_transaction_config = RpcSendTransactionConfig { - skip_preflight: false, - preflight_commitment: Some(CommitmentConfig::processed().commitment), - ..RpcSendTransactionConfig::default() - }; - process_command(&config).await.unwrap(); - - // Deploy an upgradeable program + let upgrade_authority = Keypair::new(); let program_keypair = Keypair::new(); - config.signers = vec![&keypair, &upgrade_authority, &program_keypair]; - config.command = CliCommand::Program(ProgramCliCommand::Deploy { - program_location: Some(noop_path.to_str().unwrap().to_string()), - fee_payer_signer_index: 0, - program_signer_index: Some(2), - program_pubkey: Some(program_keypair.pubkey()), - buffer_signer_index: None, - buffer_pubkey: None, - upgrade_authority_signer_index: 1, - is_final: false, - max_len: None, // Use None to check that it defaults to the max length - skip_fee_check: false, - compute_unit_price: None, - max_sign_attempts: 5, - auto_extend: false, - use_rpc: false, - skip_feature_verification: true, - }); - config.output_format = OutputFormat::JsonCompact; - process_command(&config).await.unwrap(); - - let (programdata_pubkey, _) = Pubkey::find_program_address( - &[program_keypair.pubkey().as_ref()], - &bpf_loader_upgradeable::id(), - ); + let ExtendProgramTestSetup { + test_validator: _test_validator, + mut config, + rpc_client, + programdata_pubkey, + program_len: max_len, + } = setup_extend_program_test( + &mint_keypair, + &keypair, + &upgrade_authority, + &program_keypair, + &noop_path, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .await; let programdata_account = rpc_client.get_account(&programdata_pubkey).await.unwrap(); let expected_len = UpgradeableLoaderState::size_of_programdata(max_len); @@ -1630,6 +1710,137 @@ async fn test_cli_program_extend_program() { assert_eq!(prev_len + 1024, programdata_account.data.len()); } +// Tests SIMD-0431 compatibility. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_cli_program_extend_program_minimum_size() { + agave_logger::setup(); + + let mut noop_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + noop_path.push("tests"); + noop_path.push("fixtures"); + noop_path.push("noop"); + noop_path.set_extension("so"); + + let mint_keypair = Keypair::new(); + let keypair = Keypair::new(); + let upgrade_authority = Keypair::new(); + let program_keypair = Keypair::new(); + let ExtendProgramTestSetup { + test_validator: _test_validator, + mut config, + rpc_client, + programdata_pubkey, + program_len: max_len, + } = setup_extend_program_test( + &mint_keypair, + &keypair, + &upgrade_authority, + &program_keypair, + &noop_path, + LoaderV3Features { + minimum_extend_program_size: true, + }, + ) + .await; + + // Wait one slot to avoid "Program was deployed in this block already" error. + wait_n_slots(&rpc_client, 1).await; + + // Attempting to extend by less than the 10 KiB minimum fails. + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + payer_signer_index: 0, + additional_bytes: 1, + }); + expect_command_failure( + &config, + "extend below minimum must be rejected by the CLI", + &format!( + "ExtendProgram requires a minimum of {MINIMUM_EXTEND_PROGRAM_BYTES} additional bytes \ + or to extend to maximum size, but only 1 were requested" + ), + ) + .await; + + // Program data account is unchanged. + let programdata_account = rpc_client.get_account(&programdata_pubkey).await.unwrap(); + let prev_len = programdata_account.data.len(); + assert_eq!( + prev_len, + UpgradeableLoaderState::size_of_programdata(max_len) + ); + + // Extending by exactly the 10 KiB minimum succeeds. + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + payer_signer_index: 0, + additional_bytes: MINIMUM_EXTEND_PROGRAM_BYTES, + }); + process_command(&config).await.unwrap(); + + let programdata_account = rpc_client.get_account(&programdata_pubkey).await.unwrap(); + assert_eq!( + prev_len + MINIMUM_EXTEND_PROGRAM_BYTES as usize, + programdata_account.data.len() + ); + + wait_n_slots(&rpc_client, 1).await; + + // Now extend it out to have headroom < MINIMUM_EXTEND_PROGRAM_BYTES. + let headroom = 1_000; + let prev_len = programdata_account.data.len(); + let new_len = MAX_PERMITTED_DATA_LENGTH as usize - headroom; + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + payer_signer_index: 0, + additional_bytes: (new_len - prev_len) as u32, + }); + process_command(&config).await.unwrap(); + + let programdata_account = rpc_client.get_account(&programdata_pubkey).await.unwrap(); + assert_eq!(new_len, programdata_account.data.len()); + + let prev_len = programdata_account.data.len(); + + // Attempting to extend by less than the headroom fails. + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + payer_signer_index: 0, + additional_bytes: 1, + }); + expect_command_failure( + &config, + "extend below headroom must be rejected by the CLI", + &format!( + "Program is {headroom} bytes from maximum size, but 1 were requested. Please re-run \ + the command with {headroom} additional bytes." + ), + ) + .await; + + wait_n_slots(&rpc_client, 1).await; + + // Extending by exactly headroom minimum succeeds. + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + payer_signer_index: 0, + additional_bytes: headroom as u32, + }); + process_command(&config).await.unwrap(); + + let programdata_account = rpc_client.get_account(&programdata_pubkey).await.unwrap(); + assert_eq!(prev_len + headroom, programdata_account.data.len()); + assert_eq!( + MAX_PERMITTED_DATA_LENGTH as usize, + programdata_account.data.len() + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_cli_program_write_buffer() { agave_logger::setup(); @@ -1647,10 +1858,15 @@ async fn test_cli_program_write_buffer() { noop_large_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -2055,7 +2271,12 @@ async fn test_cli_program_write_buffer_feature(enable_feature: bool) { program_path.set_extension("so"); let mint_keypair = Keypair::new(); - let mut test_validator_builder = test_validator_genesis(&mint_keypair); + let mut test_validator_builder = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ); // Deactivate the enable alt bn128 syscall and try to submit a program with that syscall if !enable_feature { @@ -2149,10 +2370,15 @@ async fn test_cli_program_set_buffer_authority() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -2331,10 +2557,15 @@ async fn test_cli_program_mismatch_buffer_authority() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -2462,10 +2693,15 @@ async fn test_cli_program_deploy_with_offline_signing(use_offline_signer_as_fee_ noop_large_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -2656,10 +2892,15 @@ async fn test_cli_program_show() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); @@ -2853,10 +3094,15 @@ async fn test_cli_program_dump() { noop_path.set_extension("so"); let mint_keypair = Keypair::new(); - let test_validator = test_validator_genesis(&mint_keypair) - .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) - .await - .expect("validator start failed"); + let test_validator = test_validator_genesis( + &mint_keypair, + LoaderV3Features { + minimum_extend_program_size: false, + }, + ) + .start_async_with_mint_address(&mint_keypair, SocketAddrSpace::Unspecified) + .await + .expect("validator start failed"); let mut config = CliConfig::recent_for_tests(); config.json_rpc_url = test_validator.rpc_url(); diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 86fcda45e08..fb6d8fffab6 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7710,17 +7710,17 @@ dependencies = [ [[package]] name = "solana-loader-v3-interface" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" dependencies = [ "serde", "serde_bytes", "serde_derive", "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-sdk-ids", - "solana-system-interface 2.0.0", + "solana-system-interface 3.2.0", ] [[package]] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index cf52e3ef0b3..96cc3166218 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -117,7 +117,7 @@ solana-instruction = "3.4.0" solana-instruction-error = "2.3.0" solana-keypair = "3.1.2" solana-ledger = { path = "../ledger", version = "=4.1.0-alpha.0", features = ["agave-unstable-api"] } -solana-loader-v3-interface = "6.1.0" +solana-loader-v3-interface = "6.1.1" solana-loader-v4-interface = "3.1.0" solana-loader-v4-program = { path = "../programs/loader-v4", version = "=4.1.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-local-cluster = { path = "../local-cluster", version = "=4.1.0-alpha.0", features = ["agave-unstable-api"] } diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 1e1c0914def..407be473247 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -84,6 +84,7 @@ pub struct FeatureSnapshot { pub validator_admission_ticket: bool, pub direct_account_pointers_in_program_input: bool, pub upgrade_bpf_stake_program_to_v5: bool, + pub loader_v3_minimum_extend_program_size: bool, } impl From<&AHashMap> for FeatureSnapshot { @@ -194,6 +195,9 @@ impl From<&AHashMap> for FeatureSnapshot { &direct_account_pointers_in_program_input::ID, ), upgrade_bpf_stake_program_to_v5: is_active(&upgrade_bpf_stake_program_to_v5::ID), + loader_v3_minimum_extend_program_size: is_active( + &loader_v3_minimum_extend_program_size::ID, + ), } } } @@ -356,6 +360,7 @@ impl FeatureSet { vote_account_initialize_v2: snapshot.vote_account_initialize_v2, direct_account_pointers_in_program_input: snapshot .direct_account_pointers_in_program_input, + loader_v3_minimum_extend_program_size: snapshot.loader_v3_minimum_extend_program_size, } } } @@ -1501,6 +1506,10 @@ pub mod direct_account_pointers_in_program_input { solana_pubkey::declare_id!("ptrXWLkSDMZZmZN8GAT6W5yW4EvYByfw6cRRHbXwQNS"); } +pub mod loader_v3_minimum_extend_program_size { + solana_pubkey::declare_id!("YbbRLkvenrocjGPGyoQE4wjnvYzTgfsk38NFmcYK7a5"); +} + pub mod upgrade_bpf_stake_program_to_v5 { solana_pubkey::declare_id!("STk5Xj8hdAx3sTzmtJ3QysKkq6X2A3yj73JtxttiRyk"); @@ -2549,6 +2558,10 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n upgrade_bpf_stake_program_to_v5::id(), "SIMD-0490: Upgrade BPF Stake Program to v5.0.0", ), + ( + loader_v3_minimum_extend_program_size::id(), + "SIMD-0431: Loader V3 minimum extend program size", + ), /*************** ADD NEW FEATURES HERE ***************/ /***** ADD NEW FEATURE BOOL TO `FeatureSnapshot` *****/ ] diff --git a/programs/bpf-loader-tests/Cargo.toml b/programs/bpf-loader-tests/Cargo.toml index 0ac9c2c0c9b..69455a4c1aa 100644 --- a/programs/bpf-loader-tests/Cargo.toml +++ b/programs/bpf-loader-tests/Cargo.toml @@ -18,10 +18,12 @@ targets = ["x86_64-unknown-linux-gnu"] agave-unstable-api = [] [dev-dependencies] +agave-feature-set = { workspace = true } assert_matches = { workspace = true } bincode = { workspace = true } solana-account = { workspace = true } solana-bpf-loader-program = { path = "../bpf_loader", default-features = false, features = ["agave-unstable-api"] } +solana-clock = { workspace = true } solana-instruction = { workspace = true } solana-keypair = { workspace = true } solana-loader-v3-interface = { workspace = true } diff --git a/programs/bpf-loader-tests/tests/common.rs b/programs/bpf-loader-tests/tests/common.rs index df337be0d15..503580ed7da 100644 --- a/programs/bpf-loader-tests/tests/common.rs +++ b/programs/bpf-loader-tests/tests/common.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use { + agave_feature_set::loader_v3_minimum_extend_program_size, solana_account::{AccountSharedData, state_traits::StateMut}, solana_instruction::{Instruction, error::InstructionError}, solana_keypair::Keypair, @@ -13,12 +14,25 @@ use { solana_transaction_error::TransactionError, }; -pub async fn setup_test_context() -> ProgramTestContext { - let program_test = ProgramTest::new( +pub struct LoaderV3Features { + /// SIMD-0431 + pub minimum_extend_program_size: bool, +} + +pub async fn setup_test_context(features: LoaderV3Features) -> ProgramTestContext { + let mut program_test = ProgramTest::new( "", id(), Some(solana_bpf_loader_program::Entrypoint::register), ); + + let LoaderV3Features { + minimum_extend_program_size, + } = features; + if !minimum_extend_program_size { + program_test.deactivate_feature(loader_v3_minimum_extend_program_size::id()); + } + program_test.start_with_context().await } diff --git a/programs/bpf-loader-tests/tests/extend_program_ix.rs b/programs/bpf-loader-tests/tests/extend_program_ix.rs index 674bf43a6f9..bbeba2aa5a3 100644 --- a/programs/bpf-loader-tests/tests/extend_program_ix.rs +++ b/programs/bpf-loader-tests/tests/extend_program_ix.rs @@ -1,10 +1,16 @@ use { assert_matches::assert_matches, - common::{add_upgradeable_loader_account, assert_ix_error, setup_test_context}, + common::{ + LoaderV3Features, add_upgradeable_loader_account, assert_ix_error, setup_test_context, + }, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::Clock, solana_instruction::error::InstructionError, solana_keypair::Keypair, - solana_loader_v3_interface::{instruction::extend_program, state::UpgradeableLoaderState}, + solana_loader_v3_interface::{ + instruction::{MINIMUM_EXTEND_PROGRAM_BYTES, extend_program}, + state::UpgradeableLoaderState, + }, solana_program_test::*, solana_pubkey::Pubkey, solana_sdk_ids::bpf_loader_upgradeable::id, @@ -21,7 +27,10 @@ mod common; #[tokio::test] async fn test_extend_program() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let program_file = find_file("noop.so").expect("Failed to find the file"); let data = read_file(program_file); let upgrade_authority = Keypair::new(); @@ -81,7 +90,10 @@ async fn test_extend_program() { #[tokio::test] async fn test_failed_extend_twice_in_same_slot() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let program_file = find_file("noop.so").expect("Failed to find the file"); let data = read_file(program_file); let upgrade_authority = Keypair::new(); @@ -166,7 +178,10 @@ async fn test_failed_extend_twice_in_same_slot() { #[tokio::test] async fn test_extend_program_not_upgradeable() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let program_address = Pubkey::new_unique(); let (programdata_address, _) = Pubkey::find_program_address(&[program_address.as_ref()], &id()); @@ -205,7 +220,10 @@ async fn test_extend_program_not_upgradeable() { #[tokio::test] async fn test_extend_program_by_zero_bytes() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let upgrade_authority = Keypair::new(); let program_address = Pubkey::new_unique(); @@ -245,7 +263,10 @@ async fn test_extend_program_by_zero_bytes() { #[tokio::test] async fn test_extend_program_past_max_size() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let upgrade_authority = Keypair::new(); let program_address = Pubkey::new_unique(); @@ -285,7 +306,10 @@ async fn test_extend_program_past_max_size() { #[tokio::test] async fn test_extend_program_with_invalid_payer() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let rent = context.banks_client.get_rent().await.unwrap(); let upgrade_authority_address = context.payer.pubkey(); @@ -385,7 +409,10 @@ async fn test_extend_program_with_invalid_payer() { #[tokio::test] async fn test_extend_program_without_payer() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let rent = context.banks_client.get_rent().await.unwrap(); let program_file = find_file("noop.so").expect("Failed to find the file"); @@ -464,7 +491,10 @@ async fn test_extend_program_without_payer() { #[tokio::test] async fn test_extend_program_with_invalid_system_program() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let upgrade_authority = Keypair::new(); let program_address = Pubkey::new_unique(); @@ -517,7 +547,10 @@ async fn test_extend_program_with_invalid_system_program() { #[tokio::test] async fn test_extend_program_with_mismatch_program_data() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let upgrade_authority = Keypair::new(); @@ -571,7 +604,10 @@ async fn test_extend_program_with_mismatch_program_data() { #[tokio::test] async fn test_extend_program_with_readonly_program_data() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let upgrade_authority = Keypair::new(); @@ -623,7 +659,10 @@ async fn test_extend_program_with_readonly_program_data() { #[tokio::test] async fn test_extend_program_with_invalid_program_data_state() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let program_address = Pubkey::new_unique(); @@ -661,7 +700,10 @@ async fn test_extend_program_with_invalid_program_data_state() { #[tokio::test] async fn test_extend_program_with_invalid_program_data_owner() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let program_address = Pubkey::new_unique(); @@ -702,7 +744,10 @@ async fn test_extend_program_with_invalid_program_data_owner() { #[tokio::test] async fn test_extend_program_with_readonly_program() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let upgrade_authority = Keypair::new(); @@ -754,7 +799,10 @@ async fn test_extend_program_with_readonly_program() { #[tokio::test] async fn test_extend_program_with_invalid_program_owner() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let upgrade_authority = Keypair::new(); @@ -795,7 +843,10 @@ async fn test_extend_program_with_invalid_program_owner() { #[tokio::test] async fn test_extend_program_with_invalid_program_state() { - let mut context = setup_test_context().await; + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: false, + }) + .await; let payer_address = context.payer.pubkey(); let upgrade_authority = Keypair::new(); @@ -833,3 +884,260 @@ async fn test_extend_program_with_invalid_program_state() { ) .await; } + +async fn setup_test_context_for_simd_0431_tests( + program_address: &Pubkey, + upgrade_authority_address: &Pubkey, + programdata_len: usize, +) -> ProgramTestContext { + // First set up the context with SIMD-0431 ENABLED. + let mut context = setup_test_context(LoaderV3Features { + minimum_extend_program_size: true, + }) + .await; + let program_file = find_file("noop.so").expect("Failed to find the file"); + let data = read_file(program_file); + + // Set up Program state. + let (programdata_address, _) = Pubkey::find_program_address(&[program_address.as_ref()], &id()); + add_upgradeable_loader_account( + &mut context, + program_address, + &UpgradeableLoaderState::Program { + programdata_address, + }, + UpgradeableLoaderState::size_of_program(), + |_| {}, + ) + .await; + + // Set up ProgramData state. + let programdata_data_offset = UpgradeableLoaderState::size_of_programdata_metadata(); + add_upgradeable_loader_account( + &mut context, + &programdata_address, + &UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: Some(*upgrade_authority_address), + }, + programdata_len, + |account| { + let end = programdata_data_offset.saturating_add(data.len()); + account.data_as_mut_slice()[programdata_data_offset..end].copy_from_slice(&data) + }, + ) + .await; + + context +} + +#[tokio::test] +async fn test_extend_program_minimum_size_requirement() { + let program_address = Pubkey::new_unique(); + let upgrade_authority = Keypair::new(); + let starting_programdata_len = (MINIMUM_EXTEND_PROGRAM_BYTES * 4) as usize; + + let mut context = setup_test_context_for_simd_0431_tests( + &program_address, + &upgrade_authority.pubkey(), + starting_programdata_len, + ) + .await; + + // Anything below the minimum size requirement should fail. + for additional_bytes in [1, 69, 420, 10_000, MINIMUM_EXTEND_PROGRAM_BYTES - 1] { + let payer_address = context.payer.pubkey(); + assert_ix_error( + &mut context, + extend_program(&program_address, Some(&payer_address), additional_bytes), + None, + InstructionError::InvalidArgument, + "should fail because the requested extension is below the minimum", + ) + .await; + } + + // Anything at or above the minimum size requirement should succeed. + let mut programdata_len = starting_programdata_len; + let (programdata_address, _) = Pubkey::find_program_address(&[program_address.as_ref()], &id()); + for additional_bytes in [ + MINIMUM_EXTEND_PROGRAM_BYTES, + MINIMUM_EXTEND_PROGRAM_BYTES + 1, + ] { + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let transaction = Transaction::new_signed_with_payer( + &[extend_program( + &program_address, + Some(&payer.pubkey()), + additional_bytes, + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); + let updated_program_data_account = client + .get_account(programdata_address) + .await + .unwrap() + .unwrap(); + + let expected_new_len = programdata_len + (additional_bytes as usize); + assert_eq!(updated_program_data_account.data().len(), expected_new_len,); + programdata_len = expected_new_len; + + let clock = client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1).unwrap(); + } +} + +#[tokio::test] +async fn test_extend_program_minimum_size_requirement_at_matching_headroom() { + // Set the programdata length so that the headroom is exactly + // MAX_PERMITTED_DATA_LENGTH - MINIMUM_EXTEND_PROGRAM_BYTES and ensure the + // minimum requirement applies. + + let program_address = Pubkey::new_unique(); + let upgrade_authority = Keypair::new(); + let programdata_len = + (MAX_PERMITTED_DATA_LENGTH as usize) - (MINIMUM_EXTEND_PROGRAM_BYTES as usize); + + let mut context = setup_test_context_for_simd_0431_tests( + &program_address, + &upgrade_authority.pubkey(), + programdata_len, + ) + .await; + + // Anything below the minimum size requirement should fail. + for additional_bytes in [1, 69, 420, 10_000, MINIMUM_EXTEND_PROGRAM_BYTES - 1] { + let payer_address = context.payer.pubkey(); + assert_ix_error( + &mut context, + extend_program(&program_address, Some(&payer_address), additional_bytes), + None, + InstructionError::InvalidArgument, + "should fail because the requested extension is below the minimum", + ) + .await; + } + + // Only exactly MINIMUM_EXTEND_PROGRAM_BYTES succeeds. + { + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let transaction = Transaction::new_signed_with_payer( + &[extend_program( + &program_address, + Some(&payer.pubkey()), + MINIMUM_EXTEND_PROGRAM_BYTES, + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + let (programdata_address, _) = + Pubkey::find_program_address(&[program_address.as_ref()], &id()); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); + let updated_program_data_account = client + .get_account(programdata_address) + .await + .unwrap() + .unwrap(); + + assert_eq!( + updated_program_data_account.data().len(), + programdata_len + (MINIMUM_EXTEND_PROGRAM_BYTES as usize), + ); + } +} + +#[tokio::test] +async fn test_extend_program_near_max_headroom_requirement() { + // Set the programdata length so that the headroom is less than + // MINIMUM_EXTEND_PROGRAM_BYTES and ensure the *headroom* requirement + // applies, and therefore not the minimum size requirement. + + let program_address = Pubkey::new_unique(); + let upgrade_authority = Keypair::new(); + + for headroom in [69, 420, 10_000, MINIMUM_EXTEND_PROGRAM_BYTES - 1] { + let programdata_len = (MAX_PERMITTED_DATA_LENGTH as usize) - (headroom as usize); + let mut context = setup_test_context_for_simd_0431_tests( + &program_address, + &upgrade_authority.pubkey(), + programdata_len, + ) + .await; + + // Anything below the headroom requirement should fail. + let mut additional_bytes = 1; + while additional_bytes < headroom - 1 { + let payer_address = context.payer.pubkey(); + assert_ix_error( + &mut context, + extend_program(&program_address, Some(&payer_address), additional_bytes), + None, + InstructionError::InvalidArgument, + "should fail because the requested extension is below the headroom", + ) + .await; + + additional_bytes += headroom.div_ceil(5); + } + + // The 10 KiB minimum should fail (too large). + { + let payer_address = context.payer.pubkey(); + assert_ix_error( + &mut context, + extend_program( + &program_address, + Some(&payer_address), + MINIMUM_EXTEND_PROGRAM_BYTES, + ), + None, + InstructionError::InvalidRealloc, + "should fail because the requested extension is too large", + ) + .await; + } + + // Only exactly `headroom` succeeds. + { + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let transaction = Transaction::new_signed_with_payer( + &[extend_program( + &program_address, + Some(&payer.pubkey()), + headroom, + )], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + let (programdata_address, _) = + Pubkey::find_program_address(&[program_address.as_ref()], &id()); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); + let updated_program_data_account = client + .get_account(programdata_address) + .await + .unwrap() + .unwrap(); + assert_eq!( + updated_program_data_account.data().len(), + programdata_len + (headroom as usize), + ); + } + } +} diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index c4e040e4a95..0a551e2fe3a 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -8,7 +8,8 @@ use { solana_bincode::limited_deserialize, solana_instruction::{AccountMeta, error::InstructionError}, solana_loader_v3_interface::{ - instruction::UpgradeableLoaderInstruction, state::UpgradeableLoaderState, + instruction::{MINIMUM_EXTEND_PROGRAM_BYTES, UpgradeableLoaderInstruction}, + state::UpgradeableLoaderState, }, solana_program_runtime::{ deploy_program, @@ -849,6 +850,30 @@ fn common_extend_program( return Err(InstructionError::InvalidRealloc); } + if invoke_context + .get_feature_set() + .loader_v3_minimum_extend_program_size + { + // SIMD-0431: Minimum Extend Program Size + // + // All extensions must be >= 10 KiB in additional_bytes, unless + // MAX_PERMITTED_DATA_LENGTH - current_len < 10 KiB. In that case, + // additional_bytes must be equal to the remaining free space. + let headroom = (MAX_PERMITTED_DATA_LENGTH as usize).saturating_sub(old_len); + if additional_bytes < MINIMUM_EXTEND_PROGRAM_BYTES + && (additional_bytes as usize) != headroom + { + ic_logger_msg!( + log_collector, + "ExtendProgram requires a minimum of {} additional bytes or to extend to maximum \ + size, but only {} were requested", + MINIMUM_EXTEND_PROGRAM_BYTES, + additional_bytes, + ); + return Err(InstructionError::InvalidArgument); + } + } + let clock_slot = invoke_context .environment_config .sysvar_cache() diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index eedb3a128e2..3bfa7c137a4 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7369,17 +7369,17 @@ dependencies = [ [[package]] name = "solana-loader-v3-interface" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" +checksum = "2e0538d4dbc9022e01616f1c58f2db98ece739c5d5ed4a2ef8737a953e76a2d4" dependencies = [ "serde", "serde_bytes", "serde_derive", "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-sdk-ids", - "solana-system-interface 2.0.0", + "solana-system-interface 3.2.0", ] [[package]] diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index 9a012022248..452a0277483 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -49,6 +49,7 @@ pub struct SVMFeatureSet { pub block_revenue_sharing: bool, pub vote_account_initialize_v2: bool, pub direct_account_pointers_in_program_input: bool, + pub loader_v3_minimum_extend_program_size: bool, } impl SVMFeatureSet { @@ -102,6 +103,7 @@ impl SVMFeatureSet { block_revenue_sharing: true, vote_account_initialize_v2: true, direct_account_pointers_in_program_input: true, + loader_v3_minimum_extend_program_size: true, } } }