diff --git a/Cargo.lock b/Cargo.lock index 7cda7836..35aa5de9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12232,6 +12232,7 @@ dependencies = [ "convert_case 0.8.0", "crossbeam", "crossbeam-channel", + "env_logger", "hex", "hiro-system-kit", "ipc-channel", @@ -12270,6 +12271,7 @@ dependencies = [ "solana-keypair", "solana-loader-v3-interface 6.1.0", "solana-message 3.0.0", + "solana-nonce 3.0.0", "solana-packet", "solana-program-option 3.0.0", "solana-program-pack 3.0.0", diff --git a/Cargo.toml b/Cargo.toml index dbfeec2b..c24135b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,7 @@ solana-instruction = { version = "3.0.0", default-features = false } solana-keypair = { version = "3.0.0", default-features = false } solana-loader-v3-interface = { version = "6.1.0", default-features = false } solana-message = { version = "3.0.0", default-features = false } +solana-nonce = { version = "3.0.0", default-features = false } solana-packet = { version = "3.0.0", default-features = false } solana-program-option = { version = "3.0.0", default-features = false } solana-program-pack = { version = "3.0.0", default-features = false } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d91458d8..ca47710e 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -63,6 +63,7 @@ solana-instruction = { workspace = true } solana-keypair = { workspace = true } solana-loader-v3-interface = { workspace = true } solana-message = { workspace = true } +solana-nonce = { workspace = true } solana-packet = { workspace = true } solana-program-option = { workspace = true } solana-program-pack = { workspace = true } @@ -97,6 +98,7 @@ txtx-addon-network-svm = { workspace = true } [dev-dependencies] test-case = { workspace = true } +env_logger = "*" [features] ignore_tests_ci = [] diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index a07fa38e..fef8584a 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -53,7 +53,9 @@ use solana_hash::Hash; use solana_inflation::Inflation; use solana_keypair::Keypair; use solana_loader_v3_interface::state::UpgradeableLoaderState; -use solana_message::{Message, VersionedMessage, v0::LoadedAddresses}; +use solana_message::{ + Message, VersionedMessage, inline_nonce::is_advance_nonce_instruction_data, v0::LoadedAddresses, +}; use solana_program_option::COption; use solana_pubkey::Pubkey; use solana_rpc_client_api::response::SlotInfo; @@ -761,6 +763,81 @@ impl SurfnetSvm { .any(|entry| entry.blockhash == *recent_blockhash) } + /// Validates the blockhash of a transaction, considering nonce accounts if present. + /// If the transaction uses a nonce account, the blockhash is validated against the nonce account's stored blockhash. + /// Otherwise, it is validated against the RecentBlockhashes sysvar. + /// + /// # Arguments + /// * `tx` - The transaction to validate. + /// + /// # Returns + /// `true` if the transaction blockhash is valid, `false` otherwise. + pub fn validate_transaction_blockhash(&self, tx: &VersionedTransaction) -> bool { + let recent_blockhash = tx.message.recent_blockhash(); + + let some_nonce_account_index = tx + .message + .instructions() + .get(solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize) + .filter(|instruction| { + matches!( + tx.message.static_account_keys().get(instruction.program_id_index as usize), + Some(program_id) if system_program::check_id(program_id) + ) && is_advance_nonce_instruction_data(&instruction.data) + }) + .map(|instruction| { + // nonce account is the first account in the instruction + instruction.accounts.get(0) + }); + + debug!( + "Validating tx blockhash: {}; is nonce tx?: {}", + recent_blockhash, + some_nonce_account_index.is_some() + ); + + if let Some(nonce_account_index) = some_nonce_account_index { + trace!( + "Nonce tx detected. Nonce account index: {:?}", + nonce_account_index + ); + let Some(nonce_account_index) = nonce_account_index else { + return false; + }; + + let Some(nonce_account_pubkey) = tx + .message + .static_account_keys() + .get(*nonce_account_index as usize) + else { + return false; + }; + + trace!("Nonce account pubkey: {:?}", nonce_account_pubkey,); + + let Some(nonce_account) = self.get_account(nonce_account_pubkey) else { + return false; + }; + trace!("Nonce account: {:?}", nonce_account); + + let Some(nonce_data) = + bincode::deserialize::(&nonce_account.data).ok() + else { + return false; + }; + trace!("Nonce account data: {:?}", nonce_data); + + let nonce_state = nonce_data.state(); + let initialized_state = match nonce_state { + solana_nonce::state::State::Uninitialized => return false, + solana_nonce::state::State::Initialized(data) => data, + }; + return initialized_state.blockhash() == *recent_blockhash; + } else { + self.check_blockhash_is_recent(recent_blockhash) + } + } + /// Sets an account in the local SVM state and notifies listeners. /// /// # Arguments @@ -1056,7 +1133,7 @@ impl SurfnetSvm { } self.transactions_processed += 1; - if !self.check_blockhash_is_recent(tx.message.recent_blockhash()) { + if !self.validate_transaction_blockhash(&tx) { let meta = TransactionMetadata::default(); let err = solana_transaction_error::TransactionError::BlockhashNotFound; @@ -1101,7 +1178,7 @@ impl SurfnetSvm { &self, transaction: &VersionedTransaction, ) -> ComputeUnitsEstimationResult { - if !self.check_blockhash_is_recent(transaction.message.recent_blockhash()) { + if !self.validate_transaction_blockhash(transaction) { return ComputeUnitsEstimationResult { success: false, compute_units_consumed: 0, @@ -1148,7 +1225,7 @@ impl SurfnetSvm { }); } - if !self.check_blockhash_is_recent(tx.message.recent_blockhash()) { + if !self.validate_transaction_blockhash(&tx) { let meta = TransactionMetadata::default(); let err = TransactionError::BlockhashNotFound; diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index f40c198e..41cc8b3c 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, sync::Arc, time::Duration}; +use std::{str::FromStr, sync::Arc, thread::sleep, time::Duration}; use base64::Engine; use crossbeam_channel::{unbounded, unbounded as crossbeam_unbounded}; @@ -6269,3 +6269,118 @@ async fn test_token2022_freeze_thaw() { println!("✓ All freeze/thaw operations work correctly!"); } + +use std::sync::Once; + +static INIT_LOGGER: Once = Once::new(); + +fn setup() { + INIT_LOGGER.call_once(|| { + env_logger::builder().is_test(true).try_init().unwrap(); + }); +} + +#[test] +fn test_nonce_accounts() { + setup(); + use solana_system_interface::instruction::create_nonce_account; + + let (svm_instance, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::new(); + let svm_locker = SurfnetSvmLocker::new(svm_instance); + + let payer = Keypair::new(); + let nonce_account = Keypair::new(); + println!("Payer Pubkey: {}", payer.pubkey()); + println!("Nonce Account Pubkey: {}", nonce_account.pubkey()); + println!("Nonce authority: {}", payer.pubkey()); + + svm_locker + .airdrop(&payer.pubkey(), 5 * LAMPORTS_PER_SOL) + .unwrap(); + + let nonce_rent = svm_locker.with_svm_reader(|svm_reader| { + svm_reader + .inner + .minimum_balance_for_rent_exemption(solana_nonce::state::State::size()) + }); + let create_nonce_ix = create_nonce_account( + &payer.pubkey(), + &nonce_account.pubkey(), + &payer.pubkey(), // Make the fee payer the nonce account authority + nonce_rent, + ); + + let recent_blockhash = svm_locker.latest_absolute_blockhash(); + + let create_nonce_msg = + Message::new_with_blockhash(&create_nonce_ix, Some(&payer.pubkey()), &recent_blockhash); + let create_nonce_tx = VersionedTransaction::try_new( + VersionedMessage::Legacy(create_nonce_msg), + &[&payer, &nonce_account], + ) + .unwrap(); + + let create_result = + svm_locker.with_svm_writer(|svm| svm.send_transaction(create_nonce_tx, false, false)); + assert!( + create_result.is_ok(), + "Create nonce account failed: {:?}", + create_result.err() + ); + + // Fetch and verify nonce account state + let nonce_account_data = svm_locker + .get_account_local(&nonce_account.pubkey()) + .inner + .map_account() + .expect("Failed to fetch nonce account"); + + let state: solana_nonce::versions::Versions = bincode::deserialize(&nonce_account_data.data) + .expect("Failed to deserialize nonce account state"); + + let state = state.state(); + + let nonce_hash = match state { + solana_nonce::state::State::Initialized(nonce_data) => { + println!( + "✓ Nonce account initialized with nonce: {:?}", + nonce_data.durable_nonce + ); + nonce_data.blockhash() + } + _ => panic!("Nonce account is not initialized"), + }; + + let to_pubkey = Pubkey::new_unique(); + // Use the nonce in a transaction + let transfer_ix = + solana_system_interface::instruction::transfer(&payer.pubkey(), &to_pubkey, 1_000_000); + let mut nonce_msg = Message::new_with_nonce( + vec![transfer_ix], + Some(&payer.pubkey()), + &nonce_account.pubkey(), + &payer.pubkey(), + ); + nonce_msg.recent_blockhash = nonce_hash; + let nonce_tx = + VersionedTransaction::try_new(VersionedMessage::Legacy(nonce_msg), &[&payer]).unwrap(); + + let nonce_result = + svm_locker.with_svm_writer(|svm| svm.send_transaction(nonce_tx, false, false)); + assert!( + nonce_result.is_ok(), + "Transaction using nonce failed: {:?}", + nonce_result.err() + ); + + // Verify to_pubkey received the funds + let to_account_data = svm_locker + .get_account_local(&to_pubkey) + .inner + .map_account() + .expect("Failed to fetch recipient account"); + assert_eq!( + to_account_data.lamports, 1_000_000, + "Recipient account did not receive correct amount" + ); +}