use { crate::{args::*, canonicalize_ledger_path, ledger_utils::*}, clap::{value_t, App, AppSettings, Arg, ArgMatches, SubCommand}, log::*, serde::{Deserialize, Serialize}, serde_json::Result, solana_bpf_loader_program::{ create_vm, load_program_from_bytes, serialization::serialize_parameters, syscalls::create_program_runtime_environment_v1, }, solana_clap_utils::input_parsers::pubkeys_of, solana_ledger::{ blockstore_options::{AccessType, BlockstoreRecoveryMode}, blockstore_processor::ProcessOptions, }, solana_program_runtime::{ invoke_context::InvokeContext, loaded_programs::{LoadProgramMetrics, LoadedProgramType, DELAY_VISIBILITY_SLOT_OFFSET}, with_mock_invoke_context, }, solana_rbpf::{ assembler::assemble, elf::Executable, static_analysis::Analysis, verifier::RequisiteVerifier, }, solana_runtime::{bank::Bank, runtime_config::RuntimeConfig}, solana_sdk::{ account::AccountSharedData, account_utils::StateMut, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, pubkey::Pubkey, slot_history::Slot, transaction_context::{IndexOfAccount, InstructionAccount}, }, std::{ collections::HashSet, fmt::{Debug, Formatter}, fs::File, io::{Read, Seek, Write}, path::{Path, PathBuf}, process::exit, sync::Arc, time::{Duration, Instant}, }, }; // The ELF magic number [ELFMAG0, ELFMAG1, ELFGMAG2, ELFMAG3] as defined by // https://github.com/torvalds/linux/blob/master/include/uapi/linux/elf.h const ELF_MAGIC_NUMBER: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46]; #[derive(Serialize, Deserialize, Debug)] struct Account { key: String, owner: Option, is_signer: Option, is_writable: Option, lamports: Option, data: Option>, } #[derive(Serialize, Deserialize)] struct Input { program_id: String, accounts: Vec, instruction_data: Vec, } fn load_accounts(path: &Path) -> Result { let file = File::open(path).unwrap(); let input: Input = serde_json::from_reader(file)?; info!("Program input:"); info!("program_id: {}", &input.program_id); info!("accounts {:?}", &input.accounts); info!("instruction_data {:?}", &input.instruction_data); info!("----------------------------------------"); Ok(input) } fn load_blockstore(ledger_path: &Path, arg_matches: &ArgMatches<'_>) -> Arc { let debug_keys = pubkeys_of(arg_matches, "debug_key") .map(|pubkeys| Arc::new(pubkeys.into_iter().collect::>())); let force_update_to_open = arg_matches.is_present("force_update_to_open"); let enforce_ulimit_nofile = !arg_matches.is_present("ignore_ulimit_nofile_error"); let process_options = ProcessOptions { new_hard_forks: hardforks_of(arg_matches, "hard_forks"), run_verification: false, on_halt_store_hash_raw_data_for_debug: false, run_final_accounts_hash_calc: false, halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(), debug_keys, limit_load_slot_count_from_snapshot: value_t!( arg_matches, "limit_load_slot_count_from_snapshot", usize ) .ok(), accounts_db_config: Some(get_accounts_db_config(ledger_path, arg_matches)), verify_index: false, allow_dead_slots: arg_matches.is_present("allow_dead_slots"), accounts_db_test_hash_calculation: false, accounts_db_skip_shrink: arg_matches.is_present("accounts_db_skip_shrink"), runtime_config: RuntimeConfig::default(), ..ProcessOptions::default() }; let snapshot_archive_path = value_t!(arg_matches, "snapshot_archive_path", String) .ok() .map(PathBuf::from); let incremental_snapshot_archive_path = value_t!(arg_matches, "incremental_snapshot_archive_path", String) .ok() .map(PathBuf::from); let wal_recovery_mode = arg_matches .value_of("wal_recovery_mode") .map(BlockstoreRecoveryMode::from); let genesis_config = open_genesis_config_by(ledger_path, arg_matches); info!("genesis hash: {}", genesis_config.hash()); let blockstore = open_blockstore( ledger_path, AccessType::Secondary, wal_recovery_mode, force_update_to_open, enforce_ulimit_nofile, ); let (bank_forks, ..) = load_and_process_ledger( arg_matches, &genesis_config, Arc::new(blockstore), process_options, snapshot_archive_path, incremental_snapshot_archive_path, ) .unwrap_or_else(|err| { eprintln!("Ledger loading failed: {err:?}"); exit(1); }); let bank = bank_forks.read().unwrap().working_bank(); bank } pub trait ProgramSubCommand { fn program_subcommand(self) -> Self; } impl ProgramSubCommand for App<'_, '_> { fn program_subcommand(self) -> Self { let program_arg = Arg::with_name("PROGRAM") .help( "Program file to use. This is either an ELF shared-object file to be executed, \ or an assembly file to be assembled and executed.", ) .required(true) .index(1); let max_genesis_arg = Arg::with_name("max_genesis_archive_unpacked_size") .long("max-genesis-archive-unpacked-size") .value_name("NUMBER") .takes_value(true) .default_value("10485760") .help("maximum total uncompressed size of unpacked genesis archive"); self.subcommand( SubCommand::with_name("program") .about("Run to test, debug, and analyze on-chain programs.") .setting(AppSettings::InferSubcommands) .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand( SubCommand::with_name("cfg") .about("generates Control Flow Graph of the program.") .arg(&program_arg) ) .subcommand( SubCommand::with_name("disassemble") .about("dumps disassembled code of the program.") .arg(&program_arg) ) .subcommand( SubCommand::with_name("run") .about( "The command executes on-chain programs in a mocked environment.", ) .arg( Arg::with_name("input") .help( r#"Input for the program to run on, where FILE is a name of a JSON file with input data, or BYTES is the number of 0-valued bytes to allocate for program parameters" The input data for a program execution have to be in JSON format and the following fields are required { "program_id": "DozgQiYtGbdyniV2T74xMdmjZJvYDzoRFFqw7UR5MwPK", "accounts": [ { "key": "524HMdYYBy6TAn4dK5vCcjiTmT2sxV6Xoue5EXrz22Ca", "owner": "BPFLoaderUpgradeab1e11111111111111111111111", "is_signer": false, "is_writable": true, "lamports": 1000, "data": [0, 0, 0, 3] } ], "instruction_data": [31, 32, 23, 24] } "#, ) .short("i") .long("input") .value_name("FILE / BYTES") .takes_value(true) .default_value("0"), ) .arg(&max_genesis_arg) .arg( Arg::with_name("memory") .help("Heap memory for the program to run on") .short("m") .long("memory") .value_name("BYTES") .takes_value(true) .default_value("0"), ) .arg( Arg::with_name("mode") .help( "Mode of execution, where 'interpreter' runs \ the program in the virtual machine's interpreter, 'debugger' is the same as 'interpreter' \ but hosts a GDB interface, and 'jit' precompiles the program to native machine code \ before execting it in the virtual machine.", ) .short("e") .long("mode") .takes_value(true) .value_name("VALUE") .possible_values(&["interpreter", "debugger", "jit"]) .default_value("jit"), ) .arg( Arg::with_name("instruction limit") .help("Limit the number of instructions to execute") .long("limit") .takes_value(true) .value_name("COUNT") .default_value("9223372036854775807"), ) .arg( Arg::with_name("port") .help("Port to use for the connection with a remote debugger") .long("port") .takes_value(true) .value_name("PORT") .default_value("9001"), ) .arg( Arg::with_name("trace") .help("Output instruction trace") .short("t") .long("trace") .takes_value(true) .value_name("FILE"), ) .arg(&program_arg) ) ) } } #[derive(Serialize)] struct Output { result: String, instruction_count: u64, execution_time: Duration, log: Vec, } impl Debug for Output { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Result: {}", self.result)?; writeln!(f, "Instruction Count: {}", self.instruction_count)?; writeln!(f, "Execution time: {} us", self.execution_time.as_micros())?; for line in &self.log { writeln!(f, "{line}")?; } Ok(()) } } // Replace with std::lazy::Lazy when stabilized. // https://github.com/rust-lang/rust/issues/74465 struct LazyAnalysis<'a, 'b> { analysis: Option>, executable: &'a Executable>, } impl<'a, 'b> LazyAnalysis<'a, 'b> { fn new(executable: &'a Executable>) -> Self { Self { analysis: None, executable, } } fn analyze(&mut self) -> &Analysis { if let Some(ref analysis) = self.analysis { return analysis; } self.analysis .insert(Analysis::from_executable(self.executable).unwrap()) } } fn output_trace( matches: &ArgMatches<'_>, trace: &[[u64; 12]], frame: usize, analysis: &mut LazyAnalysis, ) { if matches.value_of("trace").unwrap() == "stdout" { writeln!(&mut std::io::stdout(), "Frame {frame}").unwrap(); analysis .analyze() .disassemble_trace_log(&mut std::io::stdout(), trace) .unwrap(); } else { let filename = format!("{}.{}", matches.value_of("trace").unwrap(), frame); let mut fd = File::create(filename).unwrap(); writeln!(&fd, "Frame {frame}").unwrap(); analysis .analyze() .disassemble_trace_log(&mut fd, trace) .unwrap(); } } fn load_program<'a>( filename: &Path, program_id: Pubkey, invoke_context: &InvokeContext<'a>, ) -> Executable> { let mut file = File::open(filename).unwrap(); let mut magic = [0u8; 4]; file.read_exact(&mut magic).unwrap(); file.rewind().unwrap(); let is_elf = magic == ELF_MAGIC_NUMBER; let mut contents = Vec::new(); file.read_to_end(&mut contents).unwrap(); let slot = Slot::default(); let log_collector = invoke_context.get_log_collector(); let loader_key = bpf_loader_upgradeable::id(); let mut load_program_metrics = LoadProgramMetrics { program_id: program_id.to_string(), ..LoadProgramMetrics::default() }; let account_size = contents.len(); let program_runtime_environment = create_program_runtime_environment_v1( &invoke_context.feature_set, invoke_context.get_compute_budget(), false, /* deployment */ true, /* debugging_features */ ) .unwrap(); // Allowing mut here, since it may be needed for jit compile, which is under a config flag #[allow(unused_mut)] let mut verified_executable = if is_elf { let result = load_program_from_bytes( &invoke_context.feature_set, log_collector, &mut load_program_metrics, &contents, &loader_key, account_size, slot, Arc::new(program_runtime_environment), ); match result { Ok(loaded_program) => match loaded_program.program { LoadedProgramType::LegacyV1(program) => Ok(program), _ => unreachable!(), }, Err(err) => Err(format!("Loading executable failed: {err:?}")), } } else { let executable = assemble::( std::str::from_utf8(contents.as_slice()).unwrap(), Arc::new(program_runtime_environment), ) .unwrap(); Executable::::verified(executable) .map_err(|err| format!("Assembling executable failed: {err:?}")) } .unwrap(); #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] verified_executable.jit_compile().unwrap(); unsafe { std::mem::transmute::< Executable>, Executable>, >(verified_executable) } } enum Action { Cfg, Dis, } fn process_static_action(action: Action, matches: &ArgMatches<'_>) { let transaction_accounts = Vec::new(); let program_id = Pubkey::new_unique(); with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); let program = matches.value_of("PROGRAM").unwrap(); let verified_executable = load_program(Path::new(program), program_id, &invoke_context); let mut analysis = LazyAnalysis::new(&verified_executable); match action { Action::Cfg => { let mut file = File::create("cfg.dot").unwrap(); analysis .analyze() .visualize_graphically(&mut file, None) .unwrap(); } Action::Dis => { let stdout = std::io::stdout(); analysis.analyze().disassemble(&mut stdout.lock()).unwrap(); } }; } pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { let matches = match matches.subcommand() { ("cfg", Some(arg_matches)) => { process_static_action(Action::Cfg, arg_matches); return; } ("disassemble", Some(arg_matches)) => { process_static_action(Action::Dis, arg_matches); return; } ("run", Some(arg_matches)) => arg_matches, _ => unreachable!(), }; let ledger_path = canonicalize_ledger_path(ledger_path); let bank = load_blockstore(&ledger_path, matches); let loader_id = bpf_loader_upgradeable::id(); let mut transaction_accounts = Vec::new(); let mut instruction_accounts = Vec::new(); let mut program_id = Pubkey::new_unique(); let mut cached_account_keys = vec![]; let instruction_data = match matches.value_of("input").unwrap().parse::() { Ok(allocation_size) => { let pubkey = Pubkey::new_unique(); transaction_accounts.push(( pubkey, AccountSharedData::new(0, allocation_size, &Pubkey::new_unique()), )); instruction_accounts.push(InstructionAccount { index_in_transaction: 0, index_in_caller: 0, index_in_callee: 0, is_signer: false, is_writable: true, }); vec![] } Err(_) => { let input = load_accounts(Path::new(matches.value_of("input").unwrap())).unwrap(); program_id = input.program_id.parse::().unwrap_or_else(|err| { eprintln!( "Invalid program ID in input {}, error {}", input.program_id, err, ); program_id }); for (index, account_info) in input.accounts.into_iter().enumerate() { let pubkey = account_info.key.parse::().unwrap_or_else(|err| { eprintln!("Invalid key in input {}, error {}", account_info.key, err); exit(1); }); let data = account_info.data.unwrap_or(vec![]); let space = data.len(); let account = if let Some(account) = bank.get_account_with_fixed_root(&pubkey) { let owner = *account.owner(); if bpf_loader_upgradeable::check_id(&owner) { if let Ok(UpgradeableLoaderState::Program { programdata_address, }) = account.state() { debug!("Program data address {}", programdata_address); if bank .get_account_with_fixed_root(&programdata_address) .is_some() { cached_account_keys.push(pubkey); } } } // Override account data and lamports from input file if provided if space > 0 { let lamports = account_info.lamports.unwrap_or(account.lamports()); let mut account = AccountSharedData::new(lamports, space, &owner); account.set_data_from_slice(&data); account } else { account } } else { let owner = account_info .owner .unwrap_or(Pubkey::new_unique().to_string()); let owner = owner.parse::().unwrap_or_else(|err| { eprintln!("Invalid owner key in input {owner}, error {err}"); Pubkey::new_unique() }); let lamports = account_info.lamports.unwrap_or(0); let mut account = AccountSharedData::new(lamports, space, &owner); account.set_data_from_slice(&data); account }; transaction_accounts.push((pubkey, account)); instruction_accounts.push(InstructionAccount { index_in_transaction: index as IndexOfAccount, index_in_caller: index as IndexOfAccount, index_in_callee: index as IndexOfAccount, is_signer: account_info.is_signer.unwrap_or(false), is_writable: account_info.is_writable.unwrap_or(false), }); } input.instruction_data } }; let program_index: u16 = instruction_accounts.len().try_into().unwrap(); transaction_accounts.push(( loader_id, AccountSharedData::new(0, 0, &solana_sdk::native_loader::id()), )); transaction_accounts.push(( program_id, // ID of the loaded program. It can modify accounts with the same owner key AccountSharedData::new(0, 0, &loader_id), )); let interpreted = matches.value_of("mode").unwrap() != "jit"; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); // Adding `DELAY_VISIBILITY_SLOT_OFFSET` to slots to accommodate for delay visibility of the program let mut loaded_programs = LoadedProgramsForTxBatch::new( bank.slot() + DELAY_VISIBILITY_SLOT_OFFSET, bank.loaded_programs_cache .read() .unwrap() .environments .clone(), ); for key in cached_account_keys { loaded_programs.replenish(key, bank.load_program(&key)); debug!("Loaded program {}", key); } invoke_context.programs_loaded_for_tx_batch = &loaded_programs; invoke_context .transaction_context .get_next_instruction_context() .unwrap() .configure( &[program_index, program_index.saturating_add(1)], &instruction_accounts, &instruction_data, ); invoke_context.push().unwrap(); let (_parameter_bytes, regions, account_lengths) = serialize_parameters( invoke_context.transaction_context, invoke_context .transaction_context .get_current_instruction_context() .unwrap(), true, // should_cap_ix_accounts true, // copy_account_data ) .unwrap(); let program = matches.value_of("PROGRAM").unwrap(); let verified_executable = load_program(Path::new(program), program_id, &invoke_context); let mut analysis = LazyAnalysis::new(&verified_executable); create_vm!( vm, &verified_executable, regions, account_lengths, &mut invoke_context, ); let mut vm = vm.unwrap(); let start_time = Instant::now(); if matches.value_of("mode").unwrap() == "debugger" { vm.debug_port = Some(matches.value_of("port").unwrap().parse::().unwrap()); } let (instruction_count, result) = vm.execute_program(&verified_executable, interpreted); let duration = Instant::now() - start_time; if matches.occurrences_of("trace") > 0 { // top level trace is stored in syscall_context if let Some(Some(syscall_context)) = vm.context_object_pointer.syscall_context.last() { let trace = syscall_context.trace_log.as_slice(); output_trace(matches, trace, 0, &mut analysis); } // the remaining traces are saved in InvokeContext when // corresponding syscall_contexts are popped let traces = vm.context_object_pointer.get_traces(); for (frame, trace) in traces.iter().filter(|t| !t.is_empty()).enumerate() { output_trace(matches, trace, frame + 1, &mut analysis); } } drop(vm); let output = Output { result: format!("{result:?}"), instruction_count, execution_time: duration, log: invoke_context .get_log_collector() .unwrap() .borrow() .get_recorded_content() .to_vec(), }; match matches.value_of("output_format") { Some("json") => { println!("{}", serde_json::to_string_pretty(&output).unwrap()); } Some("json-compact") => { println!("{}", serde_json::to_string(&output).unwrap()); } _ => { println!("Program output:"); println!("{output:?}"); } } }