diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 8793284fa4..49a3b5fc0c 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -19,13 +19,15 @@ use { tx_wide_compute_cap, FeatureSet, }, hash::Hash, - instruction::{AccountMeta, CompiledInstruction, Instruction, InstructionError}, + instruction::{AccountMeta, Instruction, InstructionError}, keyed_account::{create_keyed_accounts_unified, KeyedAccount}, native_loader, pubkey::Pubkey, rent::Rent, saturating_add_assign, - transaction_context::{InstructionAccount, TransactionAccount, TransactionContext}, + transaction_context::{ + InstructionAccount, InstructionContext, TransactionAccount, TransactionContext, + }, }, std::{borrow::Cow, cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc, sync::Arc}, }; @@ -408,8 +410,9 @@ impl<'a> InvokeContext<'a> { self.transaction_context.pop() } - /// Current depth of the invocation stack - pub fn get_invoke_depth(&self) -> usize { + /// Current height of the invocation stack, top level instructions are height + /// `solana_sdk::instruction::TRANSACTION_LEVEL_STACK_HEIGHT` + pub fn get_stack_height(&self) -> usize { self.transaction_context .get_instruction_context_stack_height() } @@ -798,11 +801,13 @@ impl<'a> InvokeContext<'a> { .map(|index| *self.transaction_context.get_key_of_account_at_index(*index)) .unwrap_or_else(native_loader::id); - let is_lowest_invocation_level = self + let stack_height = self .transaction_context - .get_instruction_context_stack_height() - == 0; - if !is_lowest_invocation_level { + .get_instruction_context_stack_height(); + + let is_top_level_instruction = stack_height == 0; + + if !is_top_level_instruction { // Verify the calling program hasn't misbehaved let mut verify_caller_time = Measure::start("verify_caller_time"); let verify_caller_result = self.verify_and_update(instruction_accounts, true); @@ -816,20 +821,10 @@ impl<'a> InvokeContext<'a> { ); verify_caller_result?; - // Record instruction - let compiled_instruction = CompiledInstruction { - program_id_index: self - .transaction_context - .find_index_of_account(&program_id) - .unwrap_or(0) as u8, - data: instruction_data.to_vec(), - accounts: instruction_accounts - .iter() - .map(|instruction_account| instruction_account.index_in_transaction as u8) - .collect(), - }; - self.transaction_context - .record_compiled_instruction(compiled_instruction); + self.transaction_context.record_instruction( + stack_height.saturating_add(1), + InstructionContext::new(program_indices, instruction_accounts, instruction_data), + ); } let result = self @@ -848,7 +843,7 @@ impl<'a> InvokeContext<'a> { // Verify the called program has not misbehaved let mut verify_callee_time = Measure::start("verify_callee_time"); let result = execution_result.and_then(|_| { - if is_lowest_invocation_level { + if is_top_level_instruction { self.verify(instruction_accounts, program_indices) } else { self.verify_and_update(instruction_accounts, false) @@ -995,6 +990,24 @@ impl<'a> InvokeContext<'a> { pub fn get_sysvar_cache(&self) -> &SysvarCache { &self.sysvar_cache } + + /// Get instruction trace + pub fn get_instruction_trace(&self) -> &[Vec<(usize, InstructionContext)>] { + self.transaction_context.get_instruction_trace() + } + + // Get pubkey of account at index + pub fn get_key_of_account_at_index(&self, index_in_transaction: usize) -> &Pubkey { + self.transaction_context + .get_key_of_account_at_index(index_in_transaction) + } + + /// Get an instruction context + pub fn get_instruction_context_at(&self, level: usize) -> Option<&InstructionContext> { + self.transaction_context + .get_instruction_context_at(level) + .ok() + } } pub struct MockInvokeContextPreparation { diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index c344459d40..f962ba92d7 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -105,7 +105,7 @@ pub fn builtin_process_instruction( stable_log::program_invoke( &log_collector, program_id, - invoke_context.get_invoke_depth(), + invoke_context.get_stack_height(), ); // Copy indices_in_instruction into a HashSet to ensure there are no duplicates @@ -255,7 +255,7 @@ impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { stable_log::program_invoke( &log_collector, &instruction.program_id, - invoke_context.get_invoke_depth(), + invoke_context.get_stack_height(), ); let signers = signers_seeds diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index e2e2aad972..c4c80ee66c 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -3017,6 +3017,20 @@ dependencies = [ "solana-program 1.10.0", ] +[[package]] +name = "solana-bpf-rust-sibling-instructions" +version = "1.10.0" +dependencies = [ + "solana-program 1.10.0", +] + +[[package]] +name = "solana-bpf-rust-sibling_inner-instructions" +version = "1.10.0" +dependencies = [ + "solana-program 1.10.0", +] + [[package]] name = "solana-bpf-rust-spoof1" version = "1.10.0" diff --git a/programs/bpf/Cargo.toml b/programs/bpf/Cargo.toml index 24187b1ac0..6be6b12360 100644 --- a/programs/bpf/Cargo.toml +++ b/programs/bpf/Cargo.toml @@ -81,6 +81,8 @@ members = [ "rust/sanity", "rust/secp256k1_recover", "rust/sha", + "rust/sibling_inner_instruction", + "rust/sibling_instruction", "rust/spoof1", "rust/spoof1_system", "rust/sysvar", diff --git a/programs/bpf/build.rs b/programs/bpf/build.rs index 3285481538..4d6be86a55 100644 --- a/programs/bpf/build.rs +++ b/programs/bpf/build.rs @@ -91,6 +91,8 @@ fn main() { "sanity", "secp256k1_recover", "sha", + "sibling_inner_instruction", + "sibling_instruction", "spoof1", "spoof1_system", "upgradeable", diff --git a/programs/bpf/rust/sibling_inner_instruction/Cargo.toml b/programs/bpf/rust/sibling_inner_instruction/Cargo.toml new file mode 100644 index 0000000000..9ac9b68396 --- /dev/null +++ b/programs/bpf/rust/sibling_inner_instruction/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "solana-bpf-rust-sibling_inner-instructions" +version = "1.10.0" +description = "Solana BPF test program written in Rust" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-bpf-rust-log-data" +edition = "2021" + +[dependencies] +solana-program = { path = "../../../../sdk/program", version = "=1.10.0" } + +[features] +default = ["program"] +program = [] + +[lib] +crate-type = ["lib", "cdylib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/bpf/rust/sibling_inner_instruction/src/lib.rs b/programs/bpf/rust/sibling_inner_instruction/src/lib.rs new file mode 100644 index 0000000000..134d243a10 --- /dev/null +++ b/programs/bpf/rust/sibling_inner_instruction/src/lib.rs @@ -0,0 +1,69 @@ +//! Example Rust-based BPF program that queries sibling instructions + +#![cfg(feature = "program")] + +use solana_program::{ + account_info::AccountInfo, + entrypoint, + entrypoint::ProgramResult, + instruction::{ + get_processed_sibling_instruction, get_stack_height, AccountMeta, Instruction, + TRANSACTION_LEVEL_STACK_HEIGHT, + }, + msg, + pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + msg!("sibling inner"); + + // account 0 is mint + // account 1 is noop + // account 2 is invoke_and_return + + // Check sibling instructions + + let sibling_instruction2 = Instruction::new_with_bytes( + *accounts[2].key, + &[3], + vec![AccountMeta::new_readonly(*accounts[1].key, false)], + ); + let sibling_instruction1 = Instruction::new_with_bytes( + *accounts[1].key, + &[2], + vec![ + AccountMeta::new_readonly(*accounts[0].key, true), + AccountMeta::new_readonly(*accounts[1].key, false), + ], + ); + let sibling_instruction0 = Instruction::new_with_bytes( + *accounts[1].key, + &[1], + vec![ + AccountMeta::new_readonly(*accounts[1].key, false), + AccountMeta::new_readonly(*accounts[0].key, true), + ], + ); + + assert_eq!(TRANSACTION_LEVEL_STACK_HEIGHT + 1, get_stack_height()); + assert_eq!( + get_processed_sibling_instruction(0), + Some(sibling_instruction0) + ); + assert_eq!( + get_processed_sibling_instruction(1), + Some(sibling_instruction1) + ); + assert_eq!( + get_processed_sibling_instruction(2), + Some(sibling_instruction2) + ); + assert_eq!(get_processed_sibling_instruction(3), None); + + Ok(()) +} diff --git a/programs/bpf/rust/sibling_instruction/Cargo.toml b/programs/bpf/rust/sibling_instruction/Cargo.toml new file mode 100644 index 0000000000..951a3f799f --- /dev/null +++ b/programs/bpf/rust/sibling_instruction/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "solana-bpf-rust-sibling-instructions" +version = "1.10.0" +description = "Solana BPF test program written in Rust" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-bpf-rust-log-data" +edition = "2021" + +[dependencies] +solana-program = { path = "../../../../sdk/program", version = "=1.10.0" } + +[features] +default = ["program"] +program = [] + +[lib] +crate-type = ["lib", "cdylib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/bpf/rust/sibling_instruction/src/lib.rs b/programs/bpf/rust/sibling_instruction/src/lib.rs new file mode 100644 index 0000000000..5b62c6ee95 --- /dev/null +++ b/programs/bpf/rust/sibling_instruction/src/lib.rs @@ -0,0 +1,100 @@ +//! Example Rust-based BPF program that queries sibling instructions + +#![cfg(feature = "program")] + +use solana_program::{ + account_info::AccountInfo, + entrypoint, + entrypoint::ProgramResult, + instruction::{ + get_processed_sibling_instruction, get_stack_height, AccountMeta, Instruction, + TRANSACTION_LEVEL_STACK_HEIGHT, + }, + msg, + program::invoke, + pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + msg!("sibling"); + + // account 0 is mint + // account 1 is noop + // account 2 is invoke_and_return + // account 3 is sibling_inner + + // Invoke child instructions + + let instruction3 = Instruction::new_with_bytes( + *accounts[2].key, + &[3], + vec![AccountMeta::new_readonly(*accounts[1].key, false)], + ); + let instruction2 = Instruction::new_with_bytes( + *accounts[1].key, + &[2], + vec![ + AccountMeta::new_readonly(*accounts[0].key, true), + AccountMeta::new_readonly(*accounts[1].key, false), + ], + ); + let instruction1 = Instruction::new_with_bytes( + *accounts[1].key, + &[1], + vec![ + AccountMeta::new_readonly(*accounts[1].key, false), + AccountMeta::new_readonly(*accounts[0].key, true), + ], + ); + let instruction0 = Instruction::new_with_bytes( + *accounts[3].key, + &[0], + vec![ + AccountMeta::new_readonly(*accounts[0].key, false), + AccountMeta::new_readonly(*accounts[1].key, false), + AccountMeta::new_readonly(*accounts[2].key, false), + AccountMeta::new_readonly(*accounts[3].key, false), + ], + ); + invoke(&instruction3, accounts)?; + invoke(&instruction2, accounts)?; + invoke(&instruction1, accounts)?; + invoke(&instruction0, accounts)?; + + // Check sibling instructions + + let sibling_instruction1 = Instruction::new_with_bytes( + *accounts[1].key, + &[43], + vec![ + AccountMeta::new_readonly(*accounts[1].key, false), + AccountMeta::new(*accounts[0].key, true), + ], + ); + let sibling_instruction0 = Instruction::new_with_bytes( + *accounts[1].key, + &[42], + vec![ + AccountMeta::new(*accounts[0].key, true), + AccountMeta::new_readonly(*accounts[1].key, false), + ], + ); + + assert_eq!(TRANSACTION_LEVEL_STACK_HEIGHT, get_stack_height()); + assert_eq!( + get_processed_sibling_instruction(0), + Some(sibling_instruction0) + ); + assert_eq!( + get_processed_sibling_instruction(1), + Some(sibling_instruction1) + ); + assert_eq!(get_processed_sibling_instruction(2), None); + + Ok(()) +} diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index f2abc0cc6d..27f78565f3 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -3343,3 +3343,79 @@ fn test_program_bpf_realloc_invoke() { TransactionError::InstructionError(0, InstructionError::InvalidRealloc) ); } + +#[test] +#[cfg(any(feature = "bpf_rust"))] +fn test_program_bpf_processed_inner_instruction() { + solana_logger::setup(); + + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(50); + let mut bank = Bank::new_for_tests(&genesis_config); + let (name, id, entrypoint) = solana_bpf_loader_program!(); + bank.add_builtin(&name, &id, entrypoint); + let bank = Arc::new(bank); + let bank_client = BankClient::new_shared(&bank); + + let sibling_program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_sibling_instructions", + ); + let sibling_inner_program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_sibling_inner_instructions", + ); + let noop_program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_noop", + ); + let invoke_and_return_program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_invoke_and_return", + ); + + let instruction2 = Instruction::new_with_bytes( + noop_program_id, + &[43], + vec![ + AccountMeta::new_readonly(noop_program_id, false), + AccountMeta::new(mint_keypair.pubkey(), true), + ], + ); + let instruction1 = Instruction::new_with_bytes( + noop_program_id, + &[42], + vec![ + AccountMeta::new(mint_keypair.pubkey(), true), + AccountMeta::new_readonly(noop_program_id, false), + ], + ); + let instruction0 = Instruction::new_with_bytes( + sibling_program_id, + &[1, 2, 3, 0, 4, 5, 6], + vec![ + AccountMeta::new(mint_keypair.pubkey(), true), + AccountMeta::new_readonly(noop_program_id, false), + AccountMeta::new_readonly(invoke_and_return_program_id, false), + AccountMeta::new_readonly(sibling_inner_program_id, false), + ], + ); + let message = Message::new( + &[instruction2, instruction1, instruction0], + Some(&mint_keypair.pubkey()), + ); + assert!(bank_client + .send_and_confirm_message(&[&mint_keypair], message) + .is_ok()); +} diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index cd78190e98..629525411e 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -307,7 +307,7 @@ fn process_instruction_common( if program.executable()? { debug_assert_eq!( first_instruction_account, - 1 - (invoke_context.get_invoke_depth() > 1) as usize, + 1 - (invoke_context.get_stack_height() > 1) as usize, ); if !check_loader_id(&program.owner()?) { @@ -1045,7 +1045,7 @@ impl Executor for BpfExecutor { ) -> Result<(), InstructionError> { let log_collector = invoke_context.get_log_collector(); let compute_meter = invoke_context.get_compute_meter(); - let invoke_depth = invoke_context.get_invoke_depth(); + let stack_height = invoke_context.get_stack_height(); let mut serialize_time = Measure::start("serialize"); let program_id = *invoke_context.transaction_context.get_program_key()?; @@ -1074,7 +1074,7 @@ impl Executor for BpfExecutor { create_vm_time.stop(); execute_time = Measure::start("execute"); - stable_log::program_invoke(&log_collector, &program_id, invoke_depth); + stable_log::program_invoke(&log_collector, &program_id, stack_height); let mut instruction_meter = ThisInstructionMeter::new(compute_meter.clone()); let before = compute_meter.borrow().get_remaining(); let result = if use_jit { diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs index 0c66c321e3..318b5a1785 100644 --- a/programs/bpf_loader/src/syscalls.rs +++ b/programs/bpf_loader/src/syscalls.rs @@ -22,14 +22,17 @@ use { blake3, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, entrypoint::{BPF_ALIGN_OF_U128, MAX_PERMITTED_DATA_INCREASE, SUCCESS}, feature_set::{ - self, blake3_syscall_enabled, disable_fees_sysvar, do_support_realloc, - fixed_memcpy_nonoverlapping_check, libsecp256k1_0_5_upgrade_enabled, - prevent_calling_precompiles_as_programs, return_data_syscall_enabled, - secp256k1_recover_syscall_enabled, sol_log_data_syscall_enabled, - update_syscall_base_costs, + self, add_get_processed_sibling_instruction_syscall, blake3_syscall_enabled, + disable_fees_sysvar, do_support_realloc, fixed_memcpy_nonoverlapping_check, + libsecp256k1_0_5_upgrade_enabled, prevent_calling_precompiles_as_programs, + return_data_syscall_enabled, secp256k1_recover_syscall_enabled, + sol_log_data_syscall_enabled, update_syscall_base_costs, }, hash::{Hasher, HASH_BYTES}, - instruction::{AccountMeta, Instruction, InstructionError}, + instruction::{ + AccountMeta, Instruction, InstructionError, ProcessedSiblingInstruction, + TRANSACTION_LEVEL_STACK_HEIGHT, + }, keccak, native_loader, precompiles::is_precompile, program::MAX_RETURN_DATA, @@ -222,6 +225,24 @@ pub fn register_syscalls( syscall_registry.register_syscall_by_name(b"sol_log_data", SyscallLogData::call)?; } + if invoke_context + .feature_set + .is_active(&add_get_processed_sibling_instruction_syscall::id()) + { + syscall_registry.register_syscall_by_name( + b"sol_get_processed_sibling_instruction", + SyscallGetProcessedSiblingInstruction::call, + )?; + } + + if invoke_context + .feature_set + .is_active(&add_get_processed_sibling_instruction_syscall::id()) + { + syscall_registry + .register_syscall_by_name(b"sol_get_stack_height", SyscallGetStackHeight::call)?; + } + Ok(syscall_registry) } @@ -262,6 +283,9 @@ pub fn bind_syscall_context_objects<'a, 'b>( let is_zk_token_sdk_enabled = invoke_context .feature_set .is_active(&feature_set::zk_token_sdk_enabled::id()); + let add_get_processed_sibling_instruction_syscall = invoke_context + .feature_set + .is_active(&add_get_processed_sibling_instruction_syscall::id()); let loader_id = invoke_context .transaction_context @@ -444,6 +468,24 @@ pub fn bind_syscall_context_objects<'a, 'b>( }), ); + // processed inner instructions + bind_feature_gated_syscall_context_object!( + vm, + add_get_processed_sibling_instruction_syscall, + Box::new(SyscallGetProcessedSiblingInstruction { + invoke_context: invoke_context.clone(), + }), + ); + + // Get stack height + bind_feature_gated_syscall_context_object!( + vm, + add_get_processed_sibling_instruction_syscall, + Box::new(SyscallGetStackHeight { + invoke_context: invoke_context.clone(), + }), + ); + // Cross-program invocation syscalls vm.bind_syscall_context_object( Box::new(SyscallInvokeSignedC { @@ -2955,6 +2997,166 @@ impl<'a, 'b> SyscallObject for SyscallLogData<'a, 'b> { } } +pub struct SyscallGetProcessedSiblingInstruction<'a, 'b> { + invoke_context: Rc>>, +} +impl<'a, 'b> SyscallObject for SyscallGetProcessedSiblingInstruction<'a, 'b> { + fn call( + &mut self, + index: u64, + meta_addr: u64, + program_id_addr: u64, + data_addr: u64, + accounts_addr: u64, + memory_mapping: &MemoryMapping, + result: &mut Result>, + ) { + let invoke_context = question_mark!( + self.invoke_context + .try_borrow() + .map_err(|_| SyscallError::InvokeContextBorrowFailed), + result + ); + let loader_id = question_mark!( + invoke_context + .transaction_context + .get_loader_key() + .map_err(SyscallError::InstructionError), + result + ); + + let budget = invoke_context.get_compute_budget(); + question_mark!( + invoke_context + .get_compute_meter() + .consume(budget.syscall_base_cost), + result + ); + + let stack_height = invoke_context.get_stack_height(); + let instruction_trace = invoke_context.get_instruction_trace(); + let instruction_context = if stack_height == TRANSACTION_LEVEL_STACK_HEIGHT { + // pick one of the top-level instructions + instruction_trace + .len() + .checked_sub(2) + .and_then(|result| result.checked_sub(index as usize)) + .and_then(|index| instruction_trace.get(index)) + .and_then(|instruction_list| instruction_list.get(0)) + } else { + // Walk the last list of inner instructions + instruction_trace.last().and_then(|inners| { + let mut current_index = 0; + inners.iter().rev().skip(1).find(|(this_stack_height, _)| { + if stack_height == *this_stack_height { + if index == current_index { + return true; + } else { + current_index += 1; + } + } + false + }) + }) + } + .map(|(_, instruction_context)| instruction_context); + + if let Some(instruction_context) = instruction_context { + let ProcessedSiblingInstruction { + data_len, + accounts_len, + } = question_mark!( + translate_type_mut::( + memory_mapping, + meta_addr, + &loader_id + ), + result + ); + + if *data_len >= instruction_context.get_instruction_data().len() + && *accounts_len == instruction_context.get_number_of_instruction_accounts() + { + let program_id = question_mark!( + translate_type_mut::(memory_mapping, program_id_addr, &loader_id), + result + ); + let data = question_mark!( + translate_slice_mut::( + memory_mapping, + data_addr, + *data_len as u64, + &loader_id, + ), + result + ); + let accounts = question_mark!( + translate_slice_mut::( + memory_mapping, + accounts_addr, + *accounts_len as u64, + &loader_id, + ), + result + ); + + *program_id = + instruction_context.get_program_id(invoke_context.transaction_context); + data.clone_from_slice(instruction_context.get_instruction_data()); + let account_metas = instruction_context + .get_instruction_accounts_metas() + .iter() + .map(|meta| AccountMeta { + pubkey: *invoke_context + .get_key_of_account_at_index(meta.index_in_transaction), + is_signer: meta.is_signer, + is_writable: meta.is_writable, + }) + .collect::>(); + accounts.clone_from_slice(account_metas.as_slice()); + } + *data_len = instruction_context.get_instruction_data().len(); + *accounts_len = instruction_context.get_number_of_instruction_accounts(); + *result = Ok(true as u64); + return; + } + *result = Ok(false as u64); + } +} + +pub struct SyscallGetStackHeight<'a, 'b> { + invoke_context: Rc>>, +} +impl<'a, 'b> SyscallObject for SyscallGetStackHeight<'a, 'b> { + fn call( + &mut self, + _arg1: u64, + _arg2: u64, + _arg3: u64, + _arg4: u64, + _arg5: u64, + _memory_mapping: &MemoryMapping, + result: &mut Result>, + ) { + let invoke_context = question_mark!( + self.invoke_context + .try_borrow() + .map_err(|_| SyscallError::InvokeContextBorrowFailed), + result + ); + + let budget = invoke_context.get_compute_budget(); + question_mark!( + invoke_context + .get_compute_meter() + .consume(budget.syscall_base_cost), + result + ); + + *result = Ok(invoke_context.get_stack_height() as u64); + } +} + #[cfg(test)] mod tests { #[allow(deprecated)] diff --git a/programs/zk-token-proof/src/lib.rs b/programs/zk-token-proof/src/lib.rs index ab8d3706c8..2635e8d770 100644 --- a/programs/zk-token-proof/src/lib.rs +++ b/programs/zk-token-proof/src/lib.rs @@ -3,7 +3,7 @@ use { bytemuck::Pod, solana_program_runtime::{ic_msg, invoke_context::InvokeContext}, - solana_sdk::instruction::InstructionError, + solana_sdk::instruction::{InstructionError, TRANSACTION_LEVEL_STACK_HEIGHT}, solana_zk_token_sdk::zk_token_proof_instruction::*, std::result::Result, }; @@ -28,7 +28,7 @@ pub fn process_instruction( input: &[u8], invoke_context: &mut InvokeContext, ) -> Result<(), InstructionError> { - if invoke_context.get_invoke_depth() != 1 { + if invoke_context.get_stack_height() != TRANSACTION_LEVEL_STACK_HEIGHT { // Not supported as an inner instruction return Err(InstructionError::UnsupportedProgramId); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 64ed3736e7..08e71ba2ea 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -130,7 +130,7 @@ use { AddressLookupError, Result, SanitizedTransaction, Transaction, TransactionError, TransactionVerificationMode, VersionedTransaction, }, - transaction_context::{TransactionAccount, TransactionContext}, + transaction_context::{InstructionTrace, TransactionAccount, TransactionContext}, }, solana_stake_program::stake_state::{ self, InflationPointCalculationEvent, PointValue, StakeState, @@ -576,7 +576,7 @@ pub struct TransactionResults { pub struct TransactionExecutionDetails { pub status: Result<()>, pub log_messages: Option>, - pub inner_instructions: Option>>, + pub inner_instructions: Option, pub durable_nonce_fee: Option, } @@ -672,12 +672,40 @@ impl TransactionBalancesSet { } pub type TransactionBalances = Vec>; -/// An ordered list of instructions that were invoked during a transaction instruction +/// An ordered list of compiled instructions that were invoked during a +/// transaction instruction pub type InnerInstructions = Vec; -/// A list of instructions that were invoked during each instruction of a transaction +/// A list of compiled instructions that were invoked during each instruction of +/// a transaction pub type InnerInstructionsList = Vec; +/// Convert from an IntrustionTrace to InnerInstructionsList +pub fn inner_instructions_list_from_instruction_trace( + instruction_trace: &InstructionTrace, +) -> InnerInstructionsList { + instruction_trace + .iter() + .map(|inner_instructions_trace| { + inner_instructions_trace + .iter() + .skip(1) + .map(|(_, instruction_context)| { + CompiledInstruction::new_from_raw_parts( + instruction_context.get_program_id_index() as u8, + instruction_context.get_instruction_data().to_vec(), + instruction_context + .get_instruction_accounts_metas() + .iter() + .map(|meta| meta.index_in_transaction as u8) + .collect(), + ) + }) + .collect() + }) + .collect() +} + /// A list of log messages emitted during a transaction pub type TransactionLogMessages = Vec; @@ -3890,14 +3918,18 @@ impl Bank { let (accounts, instruction_trace) = transaction_context.deconstruct(); loaded_transaction.accounts = accounts; + let inner_instructions = if enable_cpi_recording { + Some(inner_instructions_list_from_instruction_trace( + &instruction_trace, + )) + } else { + None + }; + TransactionExecutionResult::Executed(TransactionExecutionDetails { status, log_messages, - inner_instructions: if enable_cpi_recording { - Some(instruction_trace) - } else { - None - }, + inner_instructions, durable_nonce_fee, }) } @@ -6616,6 +6648,7 @@ pub(crate) mod tests { sysvar::rewards::Rewards, timing::duration_as_s, transaction::MAX_TX_ACCOUNT_LOCKS, + transaction_context::InstructionContext, }, solana_vote_program::{ vote_instruction, @@ -15829,4 +15862,36 @@ pub(crate) mod tests { } } } + + #[test] + fn test_inner_instructions_list_from_instruction_trace() { + let instruction_trace = vec![ + vec![ + (1, InstructionContext::new(&[], &[], &[1])), + (2, InstructionContext::new(&[], &[], &[2])), + ], + vec![], + vec![ + (1, InstructionContext::new(&[], &[], &[3])), + (2, InstructionContext::new(&[], &[], &[4])), + (3, InstructionContext::new(&[], &[], &[5])), + (2, InstructionContext::new(&[], &[], &[6])), + ], + ]; + + let inner_instructions = inner_instructions_list_from_instruction_trace(&instruction_trace); + + assert_eq!( + inner_instructions, + vec![ + vec![CompiledInstruction::new_from_raw_parts(0, vec![2], vec![])], + vec![], + vec![ + CompiledInstruction::new_from_raw_parts(0, vec![4], vec![]), + CompiledInstruction::new_from_raw_parts(0, vec![5], vec![]), + CompiledInstruction::new_from_raw_parts(0, vec![6], vec![]) + ] + ] + ); + } } diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index 68ba99670d..0610afe17a 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -20,7 +20,7 @@ fn process_instruction_with_program_logging( ) -> Result<(), InstructionError> { let logger = invoke_context.get_log_collector(); let program_id = invoke_context.transaction_context.get_program_key()?; - stable_log::program_invoke(&logger, program_id, invoke_context.get_invoke_depth()); + stable_log::program_invoke(&logger, program_id, invoke_context.get_stack_height()); let result = process_instruction(first_instruction_account, instruction_data, invoke_context); diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index 587ecee5ff..a0077e891e 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -528,7 +528,8 @@ pub fn checked_add(a: u64, b: u64) -> Result { /// default [`AccountMeta::new`] constructor creates writable accounts, this is /// a minor hazard: use [`AccountMeta::new_readonly`] to specify that an account /// is not writable. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[repr(C)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct AccountMeta { /// An account's public key. pub pubkey: Pubkey, @@ -639,8 +640,16 @@ impl CompiledInstruction { let data = serialize(data).unwrap(); Self { program_id_index: program_ids_index, - data, accounts, + data, + } + } + + pub fn new_from_raw_parts(program_id_index: u8, data: Vec, accounts: Vec) -> Self { + Self { + program_id_index, + accounts, + data, } } @@ -648,3 +657,137 @@ impl CompiledInstruction { &program_ids[self.program_id_index as usize] } } + +/// Use to query and convey information about the sibling instruction components +/// when calling the `sol_get_processed_sibling_instruction` syscall. +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] +pub struct ProcessedSiblingInstruction { + /// Length of the instruction data + pub data_len: usize, + /// Number of AccountMeta structures + pub accounts_len: usize, +} + +/// Returns a sibling instruction from the processed sibling instruction list. +/// +/// The processed sibling instruction list is a reverse-ordered list of +/// successfully processed sibling instructions. For example, given the call flow: +/// +/// A +/// B -> C -> D +/// B -> E +/// B -> F +/// +/// Then B's processed sibling instruction list is: `[A]` +/// Then F's processed sibling instruction list is: `[E, C]` +pub fn get_processed_sibling_instruction(index: usize) -> Option { + #[cfg(target_arch = "bpf")] + { + extern "C" { + fn sol_get_processed_sibling_instruction( + index: u64, + meta: *mut ProcessedSiblingInstruction, + program_id: *mut Pubkey, + data: *mut u8, + accounts: *mut AccountMeta, + ) -> u64; + } + + let mut meta = ProcessedSiblingInstruction::default(); + let mut program_id = Pubkey::default(); + + if 1 == unsafe { + sol_get_processed_sibling_instruction( + index as u64, + &mut meta, + &mut program_id, + &mut u8::default(), + &mut AccountMeta::default(), + ) + } { + let mut data = Vec::new(); + let mut accounts = Vec::new(); + data.resize_with(meta.data_len, u8::default); + accounts.resize_with(meta.accounts_len, AccountMeta::default); + + let _ = unsafe { + sol_get_processed_sibling_instruction( + index as u64, + &mut meta, + &mut program_id, + data.as_mut_ptr(), + accounts.as_mut_ptr(), + ) + }; + + Some(Instruction::new_with_bytes(program_id, &data, accounts)) + } else { + None + } + } + + #[cfg(not(target_arch = "bpf"))] + crate::program_stubs::sol_get_processed_sibling_instruction(index) +} + +// Stack height when processing transaction-level instructions +pub const TRANSACTION_LEVEL_STACK_HEIGHT: usize = 1; + +/// Get the current stack height, transaction-level instructions are height +/// TRANSACTION_LEVEL_STACK_HEIGHT, fist invoked inner instruction is height +/// TRANSACTION_LEVEL_STACK_HEIGHT + 1, etc... +pub fn get_stack_height() -> usize { + #[cfg(target_arch = "bpf")] + { + extern "C" { + fn sol_get_stack_height() -> u64; + } + + unsafe { sol_get_stack_height() as usize } + } + + #[cfg(not(target_arch = "bpf"))] + { + crate::program_stubs::sol_get_stack_height() as usize + } +} + +#[test] +fn test_account_meta_layout() { + #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] + struct AccountMetaRust { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, + } + + let account_meta_rust = AccountMetaRust::default(); + let base_rust_addr = &account_meta_rust as *const _ as u64; + let pubkey_rust_addr = &account_meta_rust.pubkey as *const _ as u64; + let is_signer_rust_addr = &account_meta_rust.is_signer as *const _ as u64; + let is_writable_rust_addr = &account_meta_rust.is_writable as *const _ as u64; + + let account_meta_c = AccountMeta::default(); + let base_c_addr = &account_meta_c as *const _ as u64; + let pubkey_c_addr = &account_meta_c.pubkey as *const _ as u64; + let is_signer_c_addr = &account_meta_c.is_signer as *const _ as u64; + let is_writable_c_addr = &account_meta_c.is_writable as *const _ as u64; + + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() + ); + assert_eq!( + pubkey_rust_addr - base_rust_addr, + pubkey_c_addr - base_c_addr + ); + assert_eq!( + is_signer_rust_addr - base_rust_addr, + is_signer_c_addr - base_c_addr + ); + assert_eq!( + is_writable_rust_addr - base_rust_addr, + is_writable_c_addr - base_c_addr + ); +} diff --git a/sdk/program/src/program_stubs.rs b/sdk/program/src/program_stubs.rs index bc96ab5276..ca31ba04b1 100644 --- a/sdk/program/src/program_stubs.rs +++ b/sdk/program/src/program_stubs.rs @@ -91,6 +91,12 @@ pub trait SyscallStubs: Sync + Send { fn sol_log_data(&self, fields: &[&[u8]]) { println!("data: {}", fields.iter().map(base64::encode).join(" ")); } + fn sol_get_processed_sibling_instruction(&self, _index: usize) -> Option { + None + } + fn sol_get_stack_height(&self) -> u64 { + 0 + } } struct DefaultSyscallStubs {} @@ -177,6 +183,17 @@ pub(crate) fn sol_log_data(data: &[&[u8]]) { SYSCALL_STUBS.read().unwrap().sol_log_data(data) } +pub(crate) fn sol_get_processed_sibling_instruction(index: usize) -> Option { + SYSCALL_STUBS + .read() + .unwrap() + .sol_get_processed_sibling_instruction(index) +} + +pub(crate) fn sol_get_stack_height() -> u64 { + SYSCALL_STUBS.read().unwrap().sol_get_stack_height() +} + /// Check that two regions do not overlap. /// /// Adapted from libcore, hidden to share with bpf_loader without being part of diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 002a32487e..36112e06e0 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -311,6 +311,10 @@ pub mod reject_vote_account_close_unless_zero_credit_epoch { solana_sdk::declare_id!("ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj"); } +pub mod add_get_processed_sibling_instruction_syscall { + solana_sdk::declare_id!("CFK1hRCNy8JJuAAY8Pb2GjLFNdCThS2qwZNe3izzBMgn"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -383,6 +387,7 @@ lazy_static! { (vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"), (spl_associated_token_account_v1_0_4::id(), "SPL Associated Token Account Program release version 1.0.4, tied to token 3.3.0 #22648"), (reject_vote_account_close_unless_zero_credit_epoch::id(), "fail vote account withdraw to 0 unless account earned 0 credits in last completed epoch"), + (add_get_processed_sibling_instruction_syscall::id(), "add add_get_processed_sibling_instruction_syscall"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction_context.rs b/sdk/src/transaction_context.rs index bef162d96f..95c7fc075a 100644 --- a/sdk/src/transaction_context.rs +++ b/sdk/src/transaction_context.rs @@ -2,7 +2,7 @@ use crate::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, - instruction::{CompiledInstruction, InstructionError}, + instruction::{InstructionError, TRANSACTION_LEVEL_STACK_HEIGHT}, lamports::LamportsError, pubkey::Pubkey, }; @@ -32,7 +32,7 @@ pub struct TransactionContext { instruction_context_capacity: usize, instruction_context_stack: Vec, number_of_instructions_at_transaction_level: usize, - instruction_trace: Vec>, + instruction_trace: InstructionTrace, return_data: (Pubkey, Vec), } @@ -60,7 +60,12 @@ impl TransactionContext { } /// Used by the bank in the runtime to write back the processed accounts and recorded instructions - pub fn deconstruct(self) -> (Vec, Vec>) { + pub fn deconstruct( + self, + ) -> ( + Vec, + Vec>, + ) { ( Vec::from(Pin::into_inner(self.account_keys)) .into_iter() @@ -126,7 +131,8 @@ impl TransactionContext { self.instruction_context_capacity } - /// Gets the level of the next InstructionContext + /// Gets instruction stack height, top-level instructions are height + /// `solana_sdk::instruction::TRANSACTION_LEVEL_STACK_HEIGHT` pub fn get_instruction_context_stack_height(&self) -> usize { self.instruction_context_stack.len() } @@ -151,17 +157,23 @@ impl TransactionContext { if self.instruction_context_stack.len() >= self.instruction_context_capacity { return Err(InstructionError::CallDepth); } + + let instruction_context = InstructionContext { + program_accounts: program_accounts.to_vec(), + instruction_accounts: instruction_accounts.to_vec(), + instruction_data: instruction_data.to_vec(), + }; if self.instruction_context_stack.is_empty() { debug_assert!( self.instruction_trace.len() < self.number_of_instructions_at_transaction_level ); - self.instruction_trace.push(Vec::new()); + self.instruction_trace.push(vec![( + TRANSACTION_LEVEL_STACK_HEIGHT, + instruction_context.clone(), + )]); } - self.instruction_context_stack.push(InstructionContext { - program_accounts: program_accounts.to_vec(), - instruction_accounts: instruction_accounts.to_vec(), - instruction_data: instruction_data.to_vec(), - }); + + self.instruction_context_stack.push(instruction_context); Ok(()) } @@ -204,17 +216,32 @@ impl TransactionContext { } /// Used by the runtime when a new CPI instruction begins - pub fn record_compiled_instruction(&mut self, instruction: CompiledInstruction) { + pub fn record_instruction(&mut self, stack_height: usize, instruction: InstructionContext) { if let Some(records) = self.instruction_trace.last_mut() { - records.push(instruction); + records.push((stack_height, instruction)); } } + + /// Returns instruction trace + pub fn get_instruction_trace(&self) -> &InstructionTrace { + &self.instruction_trace + } +} + +/// List of (stack height, instruction) for each top-level instruction +pub type InstructionTrace = Vec>; + +#[derive(Clone, Debug)] +pub struct AccountMeta { + pub index_in_transaction: usize, + pub is_signer: bool, + pub is_writable: bool, } /// Loaded instruction shared between runtime and programs. /// /// This context is valid for the entire duration of a (possibly cross program) instruction being processed. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct InstructionContext { program_accounts: Vec, instruction_accounts: Vec, @@ -222,16 +249,50 @@ pub struct InstructionContext { } impl InstructionContext { + /// New + pub fn new( + program_accounts: &[usize], + instruction_accounts: &[InstructionAccount], + instruction_data: &[u8], + ) -> Self { + InstructionContext { + program_accounts: program_accounts.to_vec(), + instruction_accounts: instruction_accounts.to_vec(), + instruction_data: instruction_data.to_vec(), + } + } + /// Number of program accounts pub fn get_number_of_program_accounts(&self) -> usize { self.program_accounts.len() } + /// Get the index of the instruction's program id + pub fn get_program_id_index(&self) -> usize { + self.program_accounts.last().cloned().unwrap_or_default() + } + + /// Get the instruction's program id + pub fn get_program_id(&self, transaction_context: &TransactionContext) -> Pubkey { + transaction_context.account_keys[self.program_accounts.last().cloned().unwrap_or_default()] + } + /// Number of accounts in this Instruction (without program accounts) pub fn get_number_of_instruction_accounts(&self) -> usize { self.instruction_accounts.len() } + pub fn get_instruction_accounts_metas(&self) -> Vec { + self.instruction_accounts + .iter() + .map(|instruction_account| AccountMeta { + index_in_transaction: instruction_account.index_in_transaction, + is_signer: instruction_account.is_signer, + is_writable: instruction_account.is_writable, + }) + .collect() + } + /// Number of accounts in this Instruction pub fn get_number_of_accounts(&self) -> usize { self.program_accounts @@ -347,6 +408,28 @@ impl InstructionContext { } result } + + /// Returns whether an account is a signer + pub fn is_signer(&self, index_in_instruction: usize) -> bool { + if index_in_instruction < self.program_accounts.len() { + false + } else { + self.instruction_accounts + [index_in_instruction.saturating_sub(self.program_accounts.len())] + .is_signer + } + } + + /// Returns whether an account is writable + pub fn is_writable(&self, index_in_instruction: usize) -> bool { + if index_in_instruction < self.program_accounts.len() { + false + } else { + self.instruction_accounts + [index_in_instruction.saturating_sub(self.program_accounts.len())] + .is_writable + } + } } /// Shared account borrowed from the TransactionContext and an InstructionContext.