From 0ee41f976fabec3f76e9b46a9a51382058072097 Mon Sep 17 00:00:00 2001 From: Jack May Date: Thu, 13 Jan 2022 13:34:36 -0800 Subject: [PATCH] Add get_processed_inner_instruction syscall --- program-runtime/src/invoke_context.rs | 18 +++ programs/bpf/Cargo.lock | 7 ++ programs/bpf/Cargo.toml | 1 + programs/bpf/build.rs | 1 + .../bpf/rust/sibling_instruction/Cargo.toml | 23 ++++ .../bpf/rust/sibling_instruction/src/lib.rs | 61 ++++++++++ programs/bpf/tests/programs.rs | 67 +++++++++-- programs/bpf_loader/src/syscalls.rs | 106 ++++++++++++++++- sdk/program/src/instruction.rs | 112 +++++++++++++++++- sdk/program/src/program_stubs.rs | 7 ++ sdk/src/feature_set.rs | 5 + 11 files changed, 398 insertions(+), 10 deletions(-) create mode 100644 programs/bpf/rust/sibling_instruction/Cargo.toml create mode 100644 programs/bpf/rust/sibling_instruction/src/lib.rs diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 5bb2cfaf2afa41..aa18533e4bc332 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -197,6 +197,7 @@ pub struct InvokeContext<'a> { pub timings: ExecuteDetailsTimings, pub blockhash: Hash, pub lamports_per_signature: u64, + pub sibling_instructions: Vec, } impl<'a> InvokeContext<'a> { @@ -233,6 +234,7 @@ impl<'a> InvokeContext<'a> { timings: ExecuteDetailsTimings::default(), blockhash, lamports_per_signature, + sibling_instructions: Vec::new(), } } @@ -815,6 +817,7 @@ impl<'a> InvokeContext<'a> { if let Some(instruction_recorder) = &self.instruction_recorder { instruction_recorder.borrow_mut().begin_next_recording(); } + self.sibling_instructions.clear(); } else { // Verify the calling program hasn't misbehaved let mut verify_caller_time = Measure::start("verify_caller_time"); @@ -1012,6 +1015,21 @@ impl<'a> InvokeContext<'a> { pub fn get_sysvar_cache(&self) -> &SysvarCache { &self.sysvar_cache } + + // Push a sibling instruction + pub fn add_sibling_instruction(&mut self, instruction: Instruction) { + self.sibling_instructions.push(instruction.clone()); + } + + /// Get a sibling instruction, reverse ordered or last added is index 0 + pub fn get_sibling_instruction(&self, index: usize) -> Option<&Instruction> { + let index = self + .sibling_instructions + .len() + .checked_sub(1)? + .checked_sub(index)?; + self.sibling_instructions.get(index) + } } pub struct MockInvokeContextPreparation { diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index bc13e1adca4d70..f6dcd06d545518 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -3001,6 +3001,13 @@ 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-spoof1" version = "1.10.0" diff --git a/programs/bpf/Cargo.toml b/programs/bpf/Cargo.toml index 24187b1ac03f98..a1934801b0e412 100644 --- a/programs/bpf/Cargo.toml +++ b/programs/bpf/Cargo.toml @@ -81,6 +81,7 @@ members = [ "rust/sanity", "rust/secp256k1_recover", "rust/sha", + "rust/sibling_instruction", "rust/spoof1", "rust/spoof1_system", "rust/sysvar", diff --git a/programs/bpf/build.rs b/programs/bpf/build.rs index 3285481538346f..398b933807ee4f 100644 --- a/programs/bpf/build.rs +++ b/programs/bpf/build.rs @@ -91,6 +91,7 @@ fn main() { "sanity", "secp256k1_recover", "sha", + "sibling_instruction", "spoof1", "spoof1_system", "upgradeable", diff --git a/programs/bpf/rust/sibling_instruction/Cargo.toml b/programs/bpf/rust/sibling_instruction/Cargo.toml new file mode 100644 index 00000000000000..951a3f799fdbda --- /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 00000000000000..a0e871dce759e5 --- /dev/null +++ b/programs/bpf/rust/sibling_instruction/src/lib.rs @@ -0,0 +1,61 @@ +//! Example Rust-based BPF program that uses sol_get_sibling_instruction syscall + +#![cfg(feature = "program")] + +use solana_program::{ + account_info::AccountInfo, + entrypoint, + entrypoint::ProgramResult, + instruction::{get_sibling_instruction, AccountMeta, Instruction}, + 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 + + let instruction3 = Instruction::new_with_bytes(*accounts[1].key, instruction_data, vec![]); + let instruction2 = Instruction::new_with_bytes( + *accounts[2].key, + instruction_data, + vec![AccountMeta::new_readonly(*accounts[1].key, false)], + ); + let instruction1 = Instruction::new_with_bytes( + *accounts[1].key, + instruction_data, + vec![ + AccountMeta::new_readonly(*accounts[0].key, true), + AccountMeta::new_readonly(*accounts[1].key, false), + ], + ); + let instruction0 = Instruction::new_with_bytes( + *accounts[1].key, + instruction_data, + vec![ + AccountMeta::new_readonly(*accounts[1].key, false), + AccountMeta::new_readonly(*accounts[0].key, true), + ], + ); + + invoke(&instruction2, accounts)?; + invoke(&instruction1, accounts)?; + invoke(&instruction0, accounts)?; + + assert_eq!(Some(instruction0), get_sibling_instruction(0)); + assert_eq!(Some(instruction1), get_sibling_instruction(1)); + assert_eq!(Some(instruction2), get_sibling_instruction(2)); + assert_eq!(Some(instruction3), get_sibling_instruction(3)); + assert!(get_sibling_instruction(4).is_none()); + + Ok(()) +} diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 50c9d122b62474..cd86f12af0600a 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -226,13 +226,16 @@ fn run_program(name: &str) -> u64 { let mut instruction_count = 0; let mut tracer = None; for i in 0..2 { - invoke_context.transaction_context.set_return_data( - *invoke_context - .transaction_context - .get_program_key() - .unwrap(), - Vec::new(), - ).unwrap(); + invoke_context + .transaction_context + .set_return_data( + *invoke_context + .transaction_context + .get_program_key() + .unwrap(), + Vec::new(), + ) + .unwrap(); let mut parameter_bytes = parameter_bytes.clone(); { let mut vm = create_vm( @@ -3325,3 +3328,53 @@ fn test_program_bpf_realloc_invoke() { TransactionError::InstructionError(0, InstructionError::InvalidRealloc) ); } + +#[test] +#[cfg(any(feature = "bpf_rust"))] +fn test_program_bpf_sibling_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 program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_sibling_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 instruction = Instruction::new_with_bytes( + 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), + ], + ); + let message = Message::new(&[instruction], Some(&mint_keypair.pubkey())); + assert!(bank_client + .send_and_confirm_message(&[&mint_keypair], message) + .is_ok()); +} diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs index de40f1dec79e09..aa70b1605d742a 100644 --- a/programs/bpf_loader/src/syscalls.rs +++ b/programs/bpf_loader/src/syscalls.rs @@ -26,10 +26,10 @@ use { 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, + update_syscall_base_costs, add_get_sibling_instruction_syscall }, hash::{Hasher, HASH_BYTES}, - instruction::{AccountMeta, Instruction, InstructionError}, + instruction::{AccountMeta, Instruction, InstructionError, SiblingLengths}, keccak, native_loader, precompiles::is_precompile, program::MAX_RETURN_DATA, @@ -222,6 +222,16 @@ 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_sibling_instruction_syscall::id()) + { + syscall_registry.register_syscall_by_name( + b"sol_get_sibling_instruction", + SyscallGetSiblingInstruction::call, + )?; + } + Ok(syscall_registry) } @@ -262,6 +272,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_sibling_instruction_syscall = invoke_context + .feature_set + .is_active(&add_get_sibling_instruction_syscall::id()); let loader_id = invoke_context .transaction_context @@ -444,6 +457,15 @@ pub fn bind_syscall_context_objects<'a, 'b>( }), ); + // sibling instructions + bind_feature_gated_syscall_context_object!( + vm, + add_get_sibling_instruction_syscall, + Box::new(SyscallGetSiblingInstruction { + invoke_context: invoke_context.clone(), + }), + ); + // Cross-program invocation syscalls vm.bind_syscall_context_object( Box::new(SyscallInvokeSignedC { @@ -2677,6 +2699,8 @@ fn call<'a, 'b: 'a>( ) .map_err(SyscallError::InstructionError)?; + invoke_context.add_sibling_instruction(instruction); + // Copy results back to caller for (callee_account_index, caller_account) in accounts.iter_mut() { if let Some(caller_account) = caller_account { @@ -2955,6 +2979,84 @@ impl<'a, 'b> SyscallObject for SyscallLogData<'a, 'b> { } } +pub struct SyscallGetSiblingInstruction<'a, 'b> { + invoke_context: Rc>>, +} +impl<'a, 'b> SyscallObject for SyscallGetSiblingInstruction<'a, 'b> { + fn call( + &mut self, + index: u64, + lengths_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 + ); + + if let Some(instruction) = invoke_context.get_sibling_instruction(index as usize) { + let SiblingLengths { + data_len, + accounts_len, + } = question_mark!( + translate_type_mut::(memory_mapping, lengths_addr, &loader_id), + result + ); + if *data_len >= instruction.data.len() as u64 + && *accounts_len >= instruction.accounts.len() as u64 + { + 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, &loader_id,), + result + ); + let accounts = question_mark!( + translate_slice_mut::( + memory_mapping, + accounts_addr, + *accounts_len, + &loader_id, + ), + result + ); + + *program_id = instruction.program_id; + data.clone_from_slice(instruction.data.as_slice()); + accounts.clone_from_slice(instruction.accounts.as_slice()); + } + *data_len = instruction.data.len() as u64; + *accounts_len = instruction.accounts.len() as u64; + *result = Ok(true as u64); + } else { + *result = Ok(false as u64); + } + } +} + #[cfg(test)] mod tests { #[allow(deprecated)] diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index e1fbb1825a845a..2de72749044e95 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -524,7 +524,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, @@ -644,3 +645,112 @@ impl CompiledInstruction { &program_ids[self.program_id_index as usize] } } + +/// Use to query and convey the lengths of the sibling instruction components +/// when calling the syscall +#[repr(C)] +#[derive(Default, Debug, Clone, Copy)] +pub struct SiblingLengths { + pub data_len: u64, + pub accounts_len: u64, +} + +/// Returns a sibling instruction from the sibling instruction list. +/// +/// The Sibling instruction list is a reverse-ordered list of successfully +/// processed inner instructions. For example, given the call flow: +/// ``` +/// A +/// B -> C -> D +/// B -> E +/// B -> F +/// ``` +/// Then B's sibling instruction list is: [F, E, D, C] +pub fn get_sibling_instruction(index: usize) -> Option { + #[cfg(target_arch = "bpf")] + { + extern "C" { + fn sol_get_sibling_instruction( + index: u64, + lengths: *mut SiblingLengths, + program_id: *mut Pubkey, + data: *mut u8, + accounts: *mut AccountMeta, + ) -> u64; + } + + let mut lengths = SiblingLengths::default(); + let mut program_id = Pubkey::default(); + + if 1 == unsafe { + sol_get_sibling_instruction( + index as u64, + &mut lengths, + &mut program_id, + &mut u8::default(), + &mut AccountMeta::default(), + ) + } { + let mut data = Vec::new(); + let mut accounts = Vec::new(); + data.resize_with(lengths.data_len as usize, u8::default); + accounts.resize_with(lengths.accounts_len as usize, AccountMeta::default); + + let _ = unsafe { + sol_get_sibling_instruction( + index as u64, + &mut lengths, + &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::get_sibling_instruction(index) +} + +#[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 b9f62575149735..88a0f1ffb753f9 100644 --- a/sdk/program/src/program_stubs.rs +++ b/sdk/program/src/program_stubs.rs @@ -91,6 +91,9 @@ pub trait SyscallStubs: Sync + Send { fn sol_log_data(&self, fields: &[&[u8]]) { println!("data: {}", fields.iter().map(base64::encode).join(" ")); } + fn get_sibling_instruction(&self, _index: usize) -> Option { + None + } } struct DefaultSyscallStubs {} @@ -177,6 +180,10 @@ pub(crate) fn sol_log_data(data: &[&[u8]]) { SYSCALL_STUBS.read().unwrap().sol_log_data(data) } +pub(crate) fn get_sibling_instruction(index: usize) -> Option { + SYSCALL_STUBS.read().unwrap().get_sibling_instruction(index) +} + /// 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 0a8ce93ae0dc1a..d6ebee045cb552 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -307,6 +307,10 @@ pub mod update_syscall_base_costs { solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ"); } +pub mod add_get_sibling_instruction_syscall { + solana_sdk::declare_id!("CFK1hRCNy8JJuAAY8Pb2GjLFNdCThS2qwZNe3izzBMgn"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -378,6 +382,7 @@ lazy_static! { (require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"), (filter_votes_outside_slot_hashes::id(), "filter vote slots older than the slot hashes history"), (update_syscall_base_costs::id(), "Update syscall base costs"), + (add_get_sibling_instruction_syscall::id(), "add get_sibling_instruction_syscall"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()