diff --git a/Cargo.lock b/Cargo.lock index ddf8f6f..c73afc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,14 +1558,15 @@ version = "0.0.6" dependencies = [ "bincode", "criterion", + "mollusk-svm-error", "mollusk-svm-fuzz-fixture", + "mollusk-svm-keys", "solana-bpf-loader-program", "solana-compute-budget", "solana-logger", "solana-program-runtime", "solana-sdk", "solana-system-program", - "thiserror", ] [[package]] @@ -1580,6 +1581,14 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "mollusk-svm-error" +version = "0.0.6" +dependencies = [ + "solana-sdk", + "thiserror", +] + [[package]] name = "mollusk-svm-fuzz-fixture" version = "0.0.6" @@ -1594,6 +1603,14 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "mollusk-svm-keys" +version = "0.0.6" +dependencies = [ + "mollusk-svm-error", + "solana-sdk", +] + [[package]] name = "multimap" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 82d24b8..62ebb60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [workspace] members = [ "bencher", + "error", "fuzz/*", "harness", + "keys", "test-programs/*", ] resolver = "2" @@ -20,7 +22,9 @@ bincode = "1.3.3" bs58 = "0.5.1" mollusk-svm = { path = "harness", version = "0.0.6" } mollusk-svm-bencher = { path = "bencher", version = "0.0.6" } +mollusk-svm-error = { path = "error", version = "0.0.6" } mollusk-svm-fuzz-fixture = { path = "fuzz/fixture", version = "0.0.6" } +mollusk-svm-keys = { path = "keys", version = "0.0.6" } num-format = "0.4.4" prost = "0.10" prost-build = "0.10" diff --git a/error/Cargo.toml b/error/Cargo.toml new file mode 100644 index 0000000..604ba68 --- /dev/null +++ b/error/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mollusk-svm-error" +description = "Errors thrown by the Mollusk SVM harness." +documentation = "https://docs.rs/mollusk-svm-error" +authors = { workspace = true } +repository = { workspace = true } +readme = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +version = { workspace = true } + +[dependencies] +solana-sdk = { workspace = true } +thiserror = { workspace = true } diff --git a/harness/src/error.rs b/error/src/error.rs similarity index 100% rename from harness/src/error.rs rename to error/src/error.rs diff --git a/error/src/lib.rs b/error/src/lib.rs new file mode 100644 index 0000000..0c6b476 --- /dev/null +++ b/error/src/lib.rs @@ -0,0 +1,3 @@ +//! Errors thrown by the Mollusk SVM harness. + +pub mod error; diff --git a/harness/Cargo.toml b/harness/Cargo.toml index 7c2989c..2fe8872 100644 --- a/harness/Cargo.toml +++ b/harness/Cargo.toml @@ -14,14 +14,15 @@ fuzz = ["dep:mollusk-svm-fuzz-fixture"] [dependencies] bincode = { workspace = true } +mollusk-svm-error = { workspace = true } mollusk-svm-fuzz-fixture = { workspace = true, optional = true } +mollusk-svm-keys = { workspace = true } solana-bpf-loader-program = { workspace = true } solana-compute-budget = { workspace = true } solana-program-runtime = { workspace = true } solana-system-program = { workspace = true } solana-sdk = { workspace = true } solana-logger = { workspace = true } -thiserror = { workspace = true } [dev-dependencies] criterion = "0.5.1" diff --git a/harness/src/accounts.rs b/harness/src/accounts.rs new file mode 100644 index 0000000..9154586 --- /dev/null +++ b/harness/src/accounts.rs @@ -0,0 +1,53 @@ +//! Instruction <-> Transaction account compilation, with key deduplication, +//! privilege handling, and program account stubbing. + +use { + mollusk_svm_keys::{ + accounts::{ + compile_instruction_accounts, compile_instruction_without_data, + compile_transaction_accounts_for_instruction, + }, + keys::KeyMap, + }, + solana_sdk::{ + account::{AccountSharedData, WritableAccount}, + instruction::Instruction, + pubkey::Pubkey, + transaction_context::{InstructionAccount, TransactionAccount}, + }, +}; + +pub struct CompiledAccounts { + pub program_id_index: u16, + pub instruction_accounts: Vec, + pub transaction_accounts: Vec, +} + +pub fn compile_accounts( + instruction: &Instruction, + accounts: &[(Pubkey, AccountSharedData)], + loader_key: Pubkey, +) -> CompiledAccounts { + let stub_out_program_account = move || { + let mut program_account = AccountSharedData::default(); + program_account.set_owner(loader_key); + program_account.set_executable(true); + program_account + }; + + let key_map = KeyMap::compile_from_instruction(instruction); + let compiled_instruction = compile_instruction_without_data(&key_map, instruction); + let instruction_accounts = compile_instruction_accounts(&key_map, &compiled_instruction); + let transaction_accounts = compile_transaction_accounts_for_instruction( + &key_map, + instruction, + accounts, + Some(Box::new(stub_out_program_account)), + ); + + CompiledAccounts { + program_id_index: compiled_instruction.program_id_index as u16, + instruction_accounts, + transaction_accounts, + } +} diff --git a/harness/src/file.rs b/harness/src/file.rs index 33cd72a..d0960f3 100644 --- a/harness/src/file.rs +++ b/harness/src/file.rs @@ -19,7 +19,7 @@ //! error reading the file. use { - crate::error::{MolluskError, MolluskPanic}, + mollusk_svm_error::error::{MolluskError, MolluskPanic}, std::{ fs::File, io::Read, diff --git a/harness/src/keys.rs b/harness/src/keys.rs deleted file mode 100644 index 89e0f09..0000000 --- a/harness/src/keys.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Instruction <-> Transaction key deduplication and privilege handling. -//! -//! Solana instructions and transactions are designed to be intentionally -//! verbosely declarative, to provide the runtime with granular directives -//! for manipulating chain state. -//! -//! As a result, when a transaction is _compiled_, many steps occur: -//! * Ensuring there is a fee payer. -//! * Ensuring there is a signature. -//! * Deduplicating account keys. -//! * Configuring the highest role awarded to each account key. -//! * ... -//! -//! Since Mollusk does not use transactions or fee payers, the deduplication -//! of account keys and handling of roles are the only two steps necessary -//! to perform under the hood within the harness. -//! -//! This implementation closely follows the implementation in the Anza SDK -//! for `Message::new_with_blockhash`. For more information, see: -//! . - -use { - crate::error::{MolluskError, MolluskPanic}, - solana_sdk::{ - account::{AccountSharedData, WritableAccount}, - instruction::Instruction, - pubkey::Pubkey, - transaction_context::{IndexOfAccount, InstructionAccount, TransactionAccount}, - }, - std::collections::HashMap, -}; - -struct KeyMap(HashMap); - -impl KeyMap { - fn compile(instruction: &Instruction) -> Self { - let mut map: HashMap = HashMap::new(); - map.entry(instruction.program_id).or_default(); - for meta in instruction.accounts.iter() { - let entry = map.entry(meta.pubkey).or_default(); - entry.0 |= meta.is_signer; - entry.1 |= meta.is_writable; - } - Self(map) - } - - fn is_signer(&self, key: &Pubkey) -> bool { - self.0.get(key).map(|(s, _)| *s).unwrap_or(false) - } - - fn is_writable(&self, key: &Pubkey) -> bool { - self.0.get(key).map(|(_, w)| *w).unwrap_or(false) - } -} - -struct Keys<'a> { - keys: Vec<&'a Pubkey>, - key_map: &'a KeyMap, -} - -impl<'a> Keys<'a> { - fn new(key_map: &'a KeyMap) -> Self { - Self { - keys: key_map.0.keys().collect(), - key_map, - } - } - - fn position(&self, key: &Pubkey) -> u8 { - self.keys.iter().position(|k| *k == key).unwrap() as u8 - } - - fn is_signer(&self, index: usize) -> bool { - self.key_map.is_signer(self.keys[index]) - } - - fn is_writable(&self, index: usize) -> bool { - self.key_map.is_writable(self.keys[index]) - } -} - -// Helper struct so Mollusk doesn't have to clone instruction data. -struct CompiledInstructionWithoutData { - program_id_index: u8, - accounts: Vec, -} - -pub struct CompiledAccounts { - pub program_id_index: u16, - pub instruction_accounts: Vec, - pub transaction_accounts: Vec, -} - -pub fn compile_accounts( - instruction: &Instruction, - accounts: &[(Pubkey, AccountSharedData)], - loader_key: Pubkey, -) -> CompiledAccounts { - let key_map = KeyMap::compile(instruction); - let keys = Keys::new(&key_map); - - let compiled_instruction = CompiledInstructionWithoutData { - program_id_index: keys.position(&instruction.program_id), - accounts: instruction - .accounts - .iter() - .map(|account_meta| keys.position(&account_meta.pubkey)) - .collect(), - }; - - let instruction_accounts: Vec = compiled_instruction - .accounts - .iter() - .enumerate() - .map(|(ix_account_index, &index_in_transaction)| { - let index_in_callee = compiled_instruction - .accounts - .get(0..ix_account_index) - .unwrap() - .iter() - .position(|&account_index| account_index == index_in_transaction) - .unwrap_or(ix_account_index) as IndexOfAccount; - let index_in_transaction = index_in_transaction as usize; - InstructionAccount { - index_in_transaction: index_in_transaction as IndexOfAccount, - index_in_caller: index_in_transaction as IndexOfAccount, - index_in_callee, - is_signer: keys.is_signer(index_in_transaction), - is_writable: keys.is_writable(index_in_transaction), - } - }) - .collect(); - - let transaction_accounts: Vec = keys - .keys - .iter() - .map(|key| { - if *key == &instruction.program_id { - (**key, stub_out_program_account(loader_key)) - } else { - let account = accounts - .iter() - .find(|(k, _)| k == *key) - .map(|(_, account)| account.clone()) - .or_panic_with(MolluskError::AccountMissing(key)); - (**key, account) - } - }) - .collect(); - - CompiledAccounts { - program_id_index: compiled_instruction.program_id_index as u16, - instruction_accounts, - transaction_accounts, - } -} - -fn stub_out_program_account(loader_key: Pubkey) -> AccountSharedData { - let mut program_account = AccountSharedData::default(); - program_account.set_owner(loader_key); - program_account.set_executable(true); - program_account -} diff --git a/harness/src/lib.rs b/harness/src/lib.rs index 7f65ec0..92253d6 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -27,23 +27,22 @@ //! * `process_and_validate_instruction`: Process an instruction and perform a //! series of checks on the result, panicking if any checks fail. -mod error; +mod accounts; pub mod file; #[cfg(feature = "fuzz")] pub mod fuzz; -mod keys; pub mod program; pub mod result; pub mod sysvar; use { crate::{ - error::{MolluskError, MolluskPanic}, program::ProgramCache, result::{Check, InstructionResult}, sysvar::Sysvars, }, - keys::CompiledAccounts, + accounts::CompiledAccounts, + mollusk_svm_error::error::{MolluskError, MolluskPanic}, solana_compute_budget::compute_budget::ComputeBudget, solana_program_runtime::{ invoke_context::{EnvironmentConfig, InvokeContext}, @@ -154,7 +153,7 @@ impl Mollusk { program_id_index, instruction_accounts, transaction_accounts, - } = crate::keys::compile_accounts(instruction, accounts, loader_key); + } = crate::accounts::compile_accounts(instruction, accounts, loader_key); let mut transaction_context = TransactionContext::new( transaction_accounts, diff --git a/keys/Cargo.toml b/keys/Cargo.toml new file mode 100644 index 0000000..57919b4 --- /dev/null +++ b/keys/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mollusk-svm-keys" +description = "SVM transaction keys utils." +documentation = "https://docs.rs/mollusk-svm-keys" +authors = { workspace = true } +repository = { workspace = true } +readme = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +version = { workspace = true } + +[dependencies] +mollusk-svm-error = { workspace = true } +solana-sdk = { workspace = true } diff --git a/keys/src/accounts.rs b/keys/src/accounts.rs new file mode 100644 index 0000000..e827f9b --- /dev/null +++ b/keys/src/accounts.rs @@ -0,0 +1,108 @@ +//! Instruction <-> Transaction account compilation. + +use { + crate::keys::KeyMap, + mollusk_svm_error::error::{MolluskError, MolluskPanic}, + solana_sdk::{ + account::AccountSharedData, + instruction::Instruction, + pubkey::Pubkey, + transaction_context::{IndexOfAccount, InstructionAccount, TransactionAccount}, + }, +}; + +// Helper struct to avoid cloning instruction data. +pub struct CompiledInstructionWithoutData { + pub program_id_index: u8, + pub accounts: Vec, +} + +pub fn compile_instruction_without_data( + key_map: &KeyMap, + instruction: &Instruction, +) -> CompiledInstructionWithoutData { + CompiledInstructionWithoutData { + program_id_index: key_map.position(&instruction.program_id).unwrap() as u8, + accounts: instruction + .accounts + .iter() + .map(|account_meta| key_map.position(&account_meta.pubkey).unwrap() as u8) + .collect(), + } +} + +pub fn compile_instruction_accounts( + key_map: &KeyMap, + compiled_instruction: &CompiledInstructionWithoutData, +) -> Vec { + compiled_instruction + .accounts + .iter() + .enumerate() + .map(|(ix_account_index, &index_in_transaction)| { + let index_in_callee = compiled_instruction + .accounts + .get(0..ix_account_index) + .unwrap() + .iter() + .position(|&account_index| account_index == index_in_transaction) + .unwrap_or(ix_account_index) as IndexOfAccount; + let index_in_transaction = index_in_transaction as usize; + InstructionAccount { + index_in_transaction: index_in_transaction as IndexOfAccount, + index_in_caller: index_in_transaction as IndexOfAccount, + index_in_callee, + is_signer: key_map.is_signer_at_index(index_in_transaction), + is_writable: key_map.is_writable_at_index(index_in_transaction), + } + }) + .collect() +} + +pub fn compile_transaction_accounts_for_instruction( + key_map: &KeyMap, + instruction: &Instruction, + accounts: &[(Pubkey, AccountSharedData)], + stub_out_program_account: Option AccountSharedData>>, +) -> Vec { + key_map + .keys() + .map(|key| { + if let Some(stub_out_program_account) = &stub_out_program_account { + if instruction.program_id == *key { + return (*key, stub_out_program_account()); + } + } + let account = accounts + .iter() + .find(|(k, _)| k == key) + .map(|(_, account)| account.clone()) + .or_panic_with(MolluskError::AccountMissing(key)); + (*key, account) + }) + .collect() +} + +pub fn compile_transaction_accounts( + key_map: &KeyMap, + instructions: &[Instruction], + accounts: &[(Pubkey, AccountSharedData)], + stub_out_program_account: Option AccountSharedData>>, +) -> Vec { + key_map + .keys() + .map(|key| { + if let Some(stub_out_program_account) = &stub_out_program_account { + if instructions.iter().any(|ix| ix.program_id == *key) { + return (*key, stub_out_program_account()); + } + } + let account = accounts + .iter() + .find(|(k, _)| k == key) + .map(|(_, account)| account.clone()) + .or_panic_with(MolluskError::AccountMissing(key)); + (*key, account) + }) + .collect() +} diff --git a/keys/src/keys.rs b/keys/src/keys.rs new file mode 100644 index 0000000..e4b236b --- /dev/null +++ b/keys/src/keys.rs @@ -0,0 +1,268 @@ +//! Instruction <-> Transaction key deduplication and privilege handling. +//! +//! Solana instructions and transactions are designed to be intentionally +//! verbosely declarative, to provide the runtime with granular directives +//! for manipulating chain state. +//! +//! As a result, when a transaction is _compiled_, many steps occur: +//! * Ensuring there is a fee payer. +//! * Ensuring there is a signature. +//! * Deduplicating account keys. +//! * Configuring the highest role awarded to each account key. +//! * ... +//! +//! This modules provides utilities for deduplicating account keys and +//! handling the highest role awarded to each account key. It can be used +//! standalone or within the other transaction helpers in this library to build +//! custom transactions for the SVM API with the required structure and roles. +//! +//! This implementation closely follows the implementation in the Anza SDK +//! for `Message::new_with_blockhash`. For more information, see: +//! . + +use { + solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + std::collections::{HashMap, HashSet}, +}; + +/// Wrapper around a hashmap of account keys and their corresponding roles +/// (`is_signer`, `is_writable`). +/// +/// On compilation, keys are awarded the highest role they are assigned in the +/// transaction, and the hash map provides deduplication. +/// +/// The map can be queried by key for `is_signer` and `is_writable` roles. +#[derive(Debug, Default)] +pub struct KeyMap { + map: HashMap, + program_ids: HashSet, +} + +impl KeyMap { + /// Add a single account meta to the key map. + pub fn add_account(&mut self, meta: &AccountMeta) { + let entry = self.map.entry(meta.pubkey).or_default(); + entry.0 |= meta.is_signer; + entry.1 |= meta.is_writable; + } + + /// Add a list of account metas to the key map. + pub fn add_accounts<'a>(&mut self, accounts: impl Iterator) { + for meta in accounts { + self.add_account(meta); + } + } + + /// Add keys from a single instruction to the key map. + pub fn add_instruction(&mut self, instruction: &Instruction) { + self.add_program(instruction.program_id); + self.add_accounts(instruction.accounts.iter()); + } + + /// Add keys from multiple instructions to the key map. + pub fn add_instructions<'a>(&mut self, instructions: impl Iterator) { + for instruction in instructions { + self.add_instruction(instruction); + } + } + + /// Add a single program ID to the key map. + pub fn add_program(&mut self, program_id: Pubkey) { + self.map.insert(program_id, (false, false)); + self.program_ids.insert(program_id); + } + + /// Add a list of program IDs to the key map. + pub fn add_programs<'a>(&mut self, program_ids: impl Iterator) { + for program_id in program_ids { + self.add_program(*program_id); + } + } + + /// Compile a new key map with the provided program IDs and accounts. + pub fn compile<'a>( + program_ids: impl Iterator, + accounts: impl Iterator, + ) -> Self { + let mut map = Self::default(); + map.add_programs(program_ids); + map.add_accounts(accounts); + map + } + + pub fn compile_from_instruction(instruction: &Instruction) -> Self { + let mut map = Self::default(); + map.add_instruction(instruction); + map + } + + /// Compile a new key map with the keys from multiple provided instructions. + pub fn compile_from_instructions<'a>( + instructions: impl Iterator, + ) -> Self { + let mut map = Self::default(); + map.add_instructions(instructions); + map + } + + /// Query the key map for the `is_invoked` role of a key. + /// + /// This role is only for program IDs designated in an instruction. + pub fn is_invoked(&self, key: &Pubkey) -> bool { + self.program_ids.contains(key) + } + + /// Query the key map for the `is_invoked` role of a key at the specified + /// index. + /// + /// This role is only for program IDs designated in an instruction. + pub fn is_invoked_at_index(&self, index: usize) -> bool { + self.map + .iter() + .nth(index) + .map(|(k, _)| self.program_ids.contains(k)) + .unwrap_or(false) + } + + /// Query the key map for the `is_signer` role of a key. + pub fn is_signer(&self, key: &Pubkey) -> bool { + self.map.get(key).map(|(s, _)| *s).unwrap_or(false) + } + + /// Query the key map for the `is_signer` role of a key at the specified + /// index. + pub fn is_signer_at_index(&self, index: usize) -> bool { + self.map + .values() + .nth(index) + .map(|(s, _)| *s) + .unwrap_or(false) + } + + /// Get the number of keys in the key map that have the `is_signer` role. + pub fn is_signer_count(&self) -> usize { + self.map.values().filter(|(s, _)| *s).count() + } + + /// Query the key map for the `is_writable` role of a key. + pub fn is_writable(&self, key: &Pubkey) -> bool { + self.map.get(key).map(|(_, w)| *w).unwrap_or(false) + } + + /// Query the key map for the `is_writable` role of a key at the specified + /// index. + pub fn is_writable_at_index(&self, index: usize) -> bool { + self.map + .values() + .nth(index) + .map(|(_, w)| *w) + .unwrap_or(false) + } + + /// Get the number of keys in the key map that have the `is_writable` role. + pub fn is_writable_count(&self) -> usize { + self.map.values().filter(|(_, w)| *w).count() + } + + /// Get the key at the specified index in the key map. + pub fn key_at_index(&self, index: usize) -> Option<&Pubkey> { + self.map.keys().nth(index) + } + + /// Get the keys in the key map. + pub fn keys(&self) -> impl Iterator { + self.map.keys() + } + + /// Get the position of a key in the key map. + /// + /// This returns its position in the hash map's keys iterator. + pub fn position(&self, key: &Pubkey) -> Option { + self.map.keys().position(|k| k == key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compile() { + let program_id1 = Pubkey::new_unique(); + let program_id2 = Pubkey::new_unique(); + + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let key3 = Pubkey::new_unique(); + + let metas1 = &[ + // Key1: writable + AccountMeta::new(key1, false), + // Key2: signer + AccountMeta::new_readonly(key2, true), + ]; + let metas2 = &[ + // Key1: signer + AccountMeta::new_readonly(key1, true), + // Key2: signer + AccountMeta::new_readonly(key2, true), + ]; + let metas3 = &[ + // Key2: readonly + AccountMeta::new_readonly(key2, false), + // Key3: readonly + AccountMeta::new_readonly(key3, false), + ]; + + let run_checks = |key_map: &KeyMap| { + // Expected roles: + // Key1: signer, writable + // Key2: signer + // Key3: readonly + assert!(key_map.is_signer(&key1)); + assert!(key_map.is_writable(&key1)); + assert!(key_map.is_signer(&key2)); + assert!(!key_map.is_writable(&key2)); + assert!(!key_map.is_signer(&key3)); + assert!(!key_map.is_writable(&key3)); + + // Try with positional arguments. + let key1_pos = key_map.position(&key1).unwrap(); + let key2_pos = key_map.position(&key2).unwrap(); + let key3_pos = key_map.position(&key3).unwrap(); + assert!(key_map.is_signer_at_index(key1_pos)); + assert!(key_map.is_writable_at_index(key1_pos)); + assert!(key_map.is_signer_at_index(key2_pos)); + assert!(!key_map.is_writable_at_index(key2_pos)); + assert!(!key_map.is_signer_at_index(key3_pos)); + assert!(!key_map.is_writable_at_index(key3_pos)); + + // Also double-check index-pubkey compatibility. + assert_eq!(key_map.key_at_index(key1_pos).unwrap(), &key1); + assert_eq!(key_map.key_at_index(key2_pos).unwrap(), &key2); + assert_eq!(key_map.key_at_index(key3_pos).unwrap(), &key3); + let program_id1_pos = key_map.position(&program_id1).unwrap(); + let program_id2_pos = key_map.position(&program_id2).unwrap(); + assert_eq!(key_map.key_at_index(program_id1_pos).unwrap(), &program_id1); + assert_eq!(key_map.key_at_index(program_id2_pos).unwrap(), &program_id2); + }; + + // With manual adds. + let mut key_map = KeyMap::default(); + key_map.add_programs([program_id1, program_id2].iter()); + key_map.add_accounts(metas1.iter()); + key_map.add_accounts(metas2.iter()); + key_map.add_accounts(metas3.iter()); + run_checks(&key_map); + + // With `compile`. + let key_map = KeyMap::compile( + [program_id1, program_id2].iter(), + metas1.iter().chain(metas2.iter()).chain(metas3.iter()), + ); + run_checks(&key_map); + } +} diff --git a/keys/src/lib.rs b/keys/src/lib.rs new file mode 100644 index 0000000..039a131 --- /dev/null +++ b/keys/src/lib.rs @@ -0,0 +1,4 @@ +//! SVM transaction keys utils. + +pub mod accounts; +pub mod keys;