diff --git a/CHANGELOG.md b/CHANGELOG.md index e59f4e8..afdd9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Add `LiteSVM::with_precompiles`. + ## [0.3.0] - 2024-10-12 ### Added diff --git a/Cargo.lock b/Cargo.lock index ea87fd1..449f844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2210,8 +2210,10 @@ version = "0.3.0" dependencies = [ "bincode", "criterion", + "ed25519-dalek", "indexmap 2.6.0", "itertools 0.12.1", + "libsecp256k1", "log", "serde", "solana-address-lookup-table-program", diff --git a/Cargo.toml b/Cargo.toml index b19f7b4..d761a3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ rust-version = "1.75.0" [workspace.dependencies] bincode = "1.3" criterion = "0.5" +ed25519-dalek = "1.0.1" indexmap = "2.6" itertools = "0.12" +libsecp256k1 = "0.6.0" litesvm = { path = "svm", version = "0.3" } log = "0.4" serde = "1.0" diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 89766cc..eb41c0c 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -35,6 +35,8 @@ thiserror.workspace = true [dev-dependencies] criterion.workspace = true +ed25519-dalek.workspace = true +libsecp256k1.workspace = true serde.workspace = true solana-program-test.workspace = true spl-token.workspace = true diff --git a/svm/src/accounts_db.rs b/svm/src/accounts_db.rs index 281d44c..e5e6d16 100644 --- a/svm/src/accounts_db.rs +++ b/svm/src/accounts_db.rs @@ -24,7 +24,7 @@ use solana_program_runtime::{ use solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, account_utils::StateMut, - nonce, + native_loader, nonce, pubkey::Pubkey, transaction::TransactionError, }; @@ -81,7 +81,10 @@ impl AccountsDb { pubkey: Pubkey, account: AccountSharedData, ) -> Result<(), LiteSVMError> { - if account.executable() && pubkey != Pubkey::default() { + if account.executable() + && pubkey != Pubkey::default() + && account.owner() != &native_loader::ID + { let loaded_program = self.load_program(&account)?; self.programs_cache .replenish(pubkey, Arc::new(loaded_program)); diff --git a/svm/src/lib.rs b/svm/src/lib.rs index e5b6ad5..721ff2e 100644 --- a/svm/src/lib.rs +++ b/svm/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../../README.md")] use itertools::Itertools; use log::error; +use precompiles::load_precompiles; use solana_bpf_loader_program::syscalls::create_program_runtime_environment_v1; use solana_compute_budget::{ compute_budget::ComputeBudget, @@ -73,6 +74,7 @@ pub mod types; mod accounts_db; mod builtin; mod history; +mod precompiles; mod spl; mod utils; @@ -115,6 +117,7 @@ impl LiteSVM { .with_builtins(None) .with_lamports(1_000_000u64.wrapping_mul(LAMPORTS_PER_SOL)) .with_sysvars() + .with_precompiles(None) .with_spl_programs() .with_sigverify(true) .with_blockhash_check(true) @@ -228,6 +231,13 @@ impl LiteSVM { self } + pub fn with_precompiles(mut self, feature_set: Option) -> Self { + let feature_set = feature_set.unwrap_or_else(FeatureSet::all_enabled); + load_precompiles(&mut self, feature_set); + + self + } + /// Returns minimum balance required to make an account with specified data length rent exempt. pub fn minimum_balance_for_rent_exemption(&self, data_len: usize) -> u64 { 1.max( diff --git a/svm/src/precompiles.rs b/svm/src/precompiles.rs new file mode 100644 index 0000000..c3de098 --- /dev/null +++ b/svm/src/precompiles.rs @@ -0,0 +1,25 @@ +use solana_sdk::{ + account::{AccountSharedData, WritableAccount}, + feature_set::FeatureSet, + native_loader, + precompiles::get_precompiles, +}; + +use crate::LiteSVM; + +pub(crate) fn load_precompiles(svm: &mut LiteSVM, feature_set: FeatureSet) { + let mut account = AccountSharedData::default(); + account.set_owner(native_loader::id()); + account.set_lamports(1); + account.set_executable(true); + + for precompile in get_precompiles() { + if precompile + .feature + .map_or(true, |feature_id| feature_set.is_active(&feature_id)) + { + svm.set_account(precompile.program_id, account.clone().into()) + .unwrap(); + } + } +} diff --git a/svm/tests/precompiles.rs b/svm/tests/precompiles.rs new file mode 100644 index 0000000..9d68e8a --- /dev/null +++ b/svm/tests/precompiles.rs @@ -0,0 +1,101 @@ +use litesvm::LiteSVM; +use solana_sdk::{ + ed25519_instruction::{self, new_ed25519_instruction}, + message::Message, + secp256k1_instruction::{self, new_secp256k1_instruction}, + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, +}; + +#[test_log::test] +fn ed25519_precompile_ok() { + let kp = Keypair::new(); + let kp_dalek = ed25519_dalek::Keypair::from_bytes(&kp.to_bytes()).unwrap(); + + let mut svm = LiteSVM::new(); + svm.airdrop(&kp.pubkey(), 10u64.pow(9)).unwrap(); + + // Act - Produce a valid ed25519 instruction. + let ix = new_ed25519_instruction(&kp_dalek, b"hello world"); + let tx = Transaction::new( + &[&kp], + Message::new(&[ix], Some(&kp.pubkey())), + svm.latest_blockhash(), + ); + let res = svm.send_transaction(tx); + + // Assert - Transaction passes. + assert!(res.is_ok()); +} + +#[test_log::test] +fn ed25519_precompile_err() { + let kp = Keypair::new(); + let kp_dalek = ed25519_dalek::Keypair::from_bytes(&kp.to_bytes()).unwrap(); + + let mut svm = LiteSVM::new(); + svm.airdrop(&kp.pubkey(), 10u64.pow(9)).unwrap(); + + // Act - Produce an invalid ed25519 instruction. + let mut ix = new_ed25519_instruction(&kp_dalek, b"hello world"); + ix.data[ed25519_instruction::DATA_START + 32] += 1; + let tx = Transaction::new( + &[&kp], + Message::new(&[ix], Some(&kp.pubkey())), + svm.latest_blockhash(), + ); + let res = svm.send_transaction(tx); + + // Assert - Transaction fails. + assert_eq!( + res.err().map(|fail| fail.err), + Some(TransactionError::InvalidAccountIndex) + ); +} + +#[test_log::test] +fn secp256k1_precompile_ok() { + let kp = Keypair::new(); + let kp_secp256k1 = libsecp256k1::SecretKey::parse_slice(&[1; 32]).unwrap(); + + let mut svm = LiteSVM::new(); + svm.airdrop(&kp.pubkey(), 10u64.pow(9)).unwrap(); + + // Act - Produce a valid secp256k1 instruction. + let ix = new_secp256k1_instruction(&kp_secp256k1, b"hello world"); + let tx = Transaction::new( + &[&kp], + Message::new(&[ix], Some(&kp.pubkey())), + svm.latest_blockhash(), + ); + let res = svm.send_transaction(tx); + + // Assert - Transaction passes. + assert!(res.is_ok()); +} + +#[test_log::test] +fn secp256k1_precompile_err() { + let kp = Keypair::new(); + let kp_secp256k1 = libsecp256k1::SecretKey::parse_slice(&[1; 32]).unwrap(); + + let mut svm = LiteSVM::new(); + svm.airdrop(&kp.pubkey(), 10u64.pow(9)).unwrap(); + + // Act - Produce an invalid secp256k1 instruction. + let mut ix = new_secp256k1_instruction(&kp_secp256k1, b"hello world"); + ix.data[secp256k1_instruction::DATA_START + 32] += 1; + let tx = Transaction::new( + &[&kp], + Message::new(&[ix], Some(&kp.pubkey())), + svm.latest_blockhash(), + ); + let res = svm.send_transaction(tx); + + // Assert - Transaction fails. + assert_eq!( + res.err().map(|fail| fail.err), + Some(TransactionError::InvalidAccountIndex) + ); +}