diff --git a/Cargo.lock b/Cargo.lock index 3a7b574c..4b19aa01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4775,6 +4775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "347d8c652d592c618ac996f2ab21f8c0b0f2da3fbbca227a6887ee61bb75f2de" dependencies = [ "agave-feature-set", + "agave-precompiles", "agave-reserved-account-keys", "agave-syscalls", "ansi_term", @@ -11210,6 +11211,7 @@ dependencies = [ "convert_case 0.8.0", "crossbeam", "crossbeam-channel", + "ed25519-dalek 1.0.1", "env_logger", "hex", "hiro-system-kit", @@ -11240,6 +11242,7 @@ dependencies = [ "solana-clock 3.0.0", "solana-commitment-config", "solana-compute-budget-interface", + "solana-ed25519-program", "solana-epoch-info", "solana-epoch-schedule 3.0.0", "solana-feature-gate-interface 3.1.0", diff --git a/Cargo.toml b/Cargo.toml index 43dc0591..60af0bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ juniper_codegen = { version = "0.16.0", default-features = false } juniper_graphql_ws = { version = "0.4.0", default-features = false } lazy_static = "1.5.0" libloading = "0.7.4" -litesvm = { version = "0.11.0", features = ["nodejs-internal"] } +litesvm = { version = "0.11.0", features = ["nodejs-internal", "precompiles"] } litesvm-token = "0.11.0" log = "0.4.27" mime_guess = { version = "2.0.4", default-features = false } @@ -112,6 +112,7 @@ solana-commitment-config = { version = "3.1", default-features = false } solana-compute-budget-interface = { version = "3.0", default-features = false } solana-epoch-info = { version = "3.1", default-features = false } solana-epoch-schedule = { version = "3.0", default-features = false } +solana-ed25519-program = { version = "3.0", default-features = false } solana-feature-gate-interface = { version = "3.1", default-features = false } solana-genesis-config = { version = "3.0", default-features = false } # solana-geyser-plugin-manager = { version = "=3.1.6", default-features = false } # Disabled: version conflicts with litesvm 0.9.1 (requires solana-instruction =3.0.0 vs ~3.1) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9c9c2a90..72711be6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -103,8 +103,10 @@ txtx-addon-network-svm = { workspace = true } [dev-dependencies] +ed25519-dalek = "1.0.1" test-case = { workspace = true } env_logger = "0.11" +solana-ed25519-program = { workspace = true } tempfile = { workspace = true } spl-token-metadata-interface = { workspace = true } diff --git a/crates/core/src/surfnet/surfnet_lite_svm.rs b/crates/core/src/surfnet/surfnet_lite_svm.rs index 72bfb098..1f7dacd4 100644 --- a/crates/core/src/surfnet/surfnet_lite_svm.rs +++ b/crates/core/src/surfnet/surfnet_lite_svm.rs @@ -60,6 +60,7 @@ impl SurfnetLiteSvm { .with_lamports(1_000_000u64.wrapping_mul(LAMPORTS_PER_SOL)) .with_sysvars() .with_default_programs() + .with_precompiles() .with_blockhash_check(false) .with_sigverify(false) } @@ -342,3 +343,63 @@ fn create_native_mint(svm: &mut SurfnetLiteSvm) { svm.set_account(spl_token_interface::native_mint::ID, account) .expect("Failed to create native mint account in SVM"); } +#[cfg(test)] +mod tests { + use ed25519_dalek::Signer as DalekSigner; + use solana_compute_budget_interface::ComputeBudgetInstruction; + use solana_ed25519_program::new_ed25519_instruction_with_signature; + use solana_keypair::Keypair; + use solana_message::{Message, VersionedMessage}; + use solana_signer::Signer; + use solana_transaction::versioned::VersionedTransaction; + + use super::*; + + fn build_ed25519_transaction( + payer: &Keypair, + blockhash: solana_hash::Hash, + ) -> VersionedTransaction { + let payer_dalek = ed25519_dalek::Keypair::from_bytes(&payer.to_bytes()) + .expect("failed to create dalek keypair"); + let message = b"surfpool ed25519 precompile regression"; + let signature = payer_dalek.sign(message); + let ed25519_ix = new_ed25519_instruction_with_signature( + message, + &signature.to_bytes(), + payer.pubkey().as_array(), + ); + let compute_budget_ixs = [ + ComputeBudgetInstruction::set_compute_unit_limit(100_000), + ComputeBudgetInstruction::set_compute_unit_price(1), + ]; + + let tx_message = Message::new_with_blockhash( + &[ + compute_budget_ixs[0].clone(), + compute_budget_ixs[1].clone(), + ed25519_ix, + ], + Some(&payer.pubkey()), + &blockhash, + ); + + VersionedTransaction::try_new(VersionedMessage::Legacy(tx_message), &[&payer]) + .expect("failed to create ed25519 transaction") + } + + #[test] + fn test_base_litesvm_settings_registers_ed25519_precompile() { + let mut svm = SurfnetLiteSvm::base_litesvm_settings(); + let payer = Keypair::new(); + svm.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL) + .expect("failed to fund test payer"); + let tx = build_ed25519_transaction(&payer, svm.latest_blockhash()); + + let result = svm.send_transaction(tx); + + assert!( + result.is_ok(), + "ed25519 precompile should be available in base_litesvm_settings" + ); + } +} diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index 0476a0d0..198d7751 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -2,6 +2,7 @@ use std::{str::FromStr, sync::Arc, time::Duration}; use base64::Engine; use crossbeam_channel::{unbounded, unbounded as crossbeam_unbounded}; +use ed25519_dalek::Signer as DalekSigner; use jsonrpc_core::{ Error, Result as JsonRpcResult, futures::future::{self, join_all}, @@ -18,6 +19,7 @@ use solana_client::{ use solana_clock::{Clock, Slot}; use solana_commitment_config::{CommitmentConfig, CommitmentLevel}; use solana_compute_budget_interface::ComputeBudgetInstruction; +use solana_ed25519_program::new_ed25519_instruction_with_signature; use solana_epoch_info::EpochInfo; use solana_hash::Hash; use solana_keypair::Keypair; @@ -752,6 +754,83 @@ async fn test_simulate_transaction_no_signers(test_type: TestType) { ); } +#[test_case(TestType::sqlite(); "with on-disk sqlite db")] +#[test_case(TestType::in_memory(); "with in-memory sqlite db")] +#[test_case(TestType::no_db(); "with no db")] +#[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))] +#[tokio::test(flavor = "multi_thread")] +async fn test_transaction_with_ed25519_instruction(test_type: TestType) { + let payer = Keypair::new(); + let (mut svm_instance, _simnet_events_rx, _geyser_events_rx) = test_type.initialize_svm(); + svm_instance + .airdrop(&payer.pubkey(), LAMPORTS_PER_SOL) + .unwrap() + .unwrap(); + let recent_blockhash = svm_instance.latest_blockhash(); + + // Issue #587 previously failed here with: + // "Transaction simulation failed: Error processing Instruction 2: Unsupported program id". + let tx = { + let payer_dalek = + ed25519_dalek::Keypair::from_bytes(&payer.to_bytes()).expect("invalid dalek keypair"); + let message = b"surfpool ed25519 precompile integration test"; + let signature = payer_dalek.sign(message); + let ed25519_ix = new_ed25519_instruction_with_signature( + message, + &signature.to_bytes(), + payer.pubkey().as_array(), + ); + let compute_budget_ixs = [ + ComputeBudgetInstruction::set_compute_unit_limit(100_000), + ComputeBudgetInstruction::set_compute_unit_price(1), + ]; + let tx_message = Message::new_with_blockhash( + &[ + compute_budget_ixs[0].clone(), + compute_budget_ixs[1].clone(), + ed25519_ix, + ], + Some(&payer.pubkey()), + &recent_blockhash, + ); + + VersionedTransaction::try_new(VersionedMessage::Legacy(tx_message), &[payer]) + .expect("Failed to create ed25519 transaction") + }; + let svm_locker = SurfnetSvmLocker::new(svm_instance); + let simulation_res = svm_locker.simulate_transaction(tx.clone(), true); + + assert!( + simulation_res.is_ok(), + "Expected ed25519 transaction simulation to succeed" + ); + + let (status_tx, status_rx) = unbounded(); + let _ = svm_locker + .process_transaction(&None, tx, status_tx, true, true) + .await + .unwrap(); + + // Wait for transaction processing + match status_rx.recv() { + Ok(TransactionStatusEvent::Success(_)) => { + println!("Transaction processed successfully"); + } + Ok(TransactionStatusEvent::SimulationFailure((error, _))) => { + panic!("Transaction simulation failed: {:?}", error); + } + Ok(TransactionStatusEvent::ExecutionFailure((error, _))) => { + panic!("Transaction execution failed: {:?}", error); + } + Ok(TransactionStatusEvent::VerificationFailure(error)) => { + panic!("Transaction verification failed: {}", error); + } + Err(e) => { + panic!("Failed to receive transaction status: {:?}", e); + } + } +} + #[cfg_attr(feature = "ignore_tests_ci", ignore = "flaky CI tests")] #[test_case(TestType::sqlite(); "with on-disk sqlite db")] #[test_case(TestType::in_memory(); "with in-memory sqlite db")]