trident_fuzz/trident/
client.rs

1use borsh::BorshDeserialize;
2use solana_sdk::instruction::Instruction;
3use solana_sdk::pubkey::Pubkey;
4use solana_sdk::transaction::TransactionError;
5use trident_svm::prelude::TridentTransactionProcessingResult;
6use trident_svm::processor::InstructionError;
7
8use crate::trident::transaction_result::TransactionResult;
9use crate::trident::Trident;
10
11use solana_sdk::account::AccountSharedData;
12use solana_sdk::account::ReadableAccount;
13use solana_sdk::account::WritableAccount;
14use solana_sdk::clock::Clock;
15use solana_sdk::hash::Hash;
16use solana_sdk::signer::Signer;
17use solana_sdk::sysvar::Sysvar;
18
19#[cfg(feature = "syscall-v2")]
20use trident_svm::types::trident_entrypoint::TridentEntrypoint;
21use trident_svm::types::trident_program::TridentProgram;
22
23impl Trident {
24    /// Processes a transaction containing one or more instructions
25    ///
26    /// This method executes the provided instructions as a single transaction and returns
27    /// the result including success/failure status and transaction logs. It also handles
28    /// fuzzing metrics collection when enabled via environment variables.
29    ///
30    /// # Arguments
31    /// * `instructions` - A slice of instructions to execute in the transaction
32    /// * `transaction_name` - A descriptive name for the transaction (used in metrics)
33    ///
34    /// # Returns
35    /// A `TransactionResult` containing the execution result and logs
36    ///
37    /// # Example
38    /// ```rust,ignore
39    /// let instructions = vec![system_instruction::transfer(&from, &to, 1000)];
40    /// let result = trident.process_transaction(&instructions, Some("Transfer SOL"));
41    /// assert!(result.is_success());
42    /// ```
43    pub fn process_transaction(
44        &mut self,
45        instructions: &[Instruction],
46        log_as: Option<&str>,
47    ) -> TransactionResult {
48        let fuzzing_metrics = std::env::var("FUZZING_METRICS");
49        let fuzzing_debug = std::env::var("TRIDENT_FUZZ_DEBUG");
50
51        if fuzzing_metrics.is_ok() && log_as.is_some() {
52            if let Some(log_as) = log_as {
53                self.fuzzing_data.add_executed_transaction(log_as);
54            }
55        }
56        if fuzzing_debug.is_ok() {
57            let tx = format!("{:#?}", instructions);
58            trident_svm::prelude::trident_svm_log::log_message(
59                &tx,
60                trident_svm::prelude::Level::Debug,
61            );
62        }
63        let processing_data = self.process_instructions(instructions);
64
65        self.handle_tx_result(&processing_data, log_as, instructions)
66    }
67
68    /// Deploys an entrypoint program to the SVM runtime
69    ///
70    /// This method is only available when the "syscall-v2" feature is enabled.
71    /// It deploys a program that serves as an entrypoint for other programs.
72    ///
73    /// # Arguments
74    /// * `_program` - The entrypoint program to deploy
75    #[cfg(feature = "syscall-v2")]
76    pub fn deploy_entrypoint(&mut self, _program: TridentEntrypoint) {
77        self.client.deploy_entrypoint_program(&_program);
78    }
79
80    /// Deploys a binary program to the SVM runtime
81    ///
82    /// This method deploys a compiled Solana program (BPF/SBF) to the runtime,
83    /// making it available for instruction execution.
84    ///
85    /// # Arguments
86    /// * `program` - The compiled program to deploy
87    pub fn deploy_program(&mut self, program: TridentProgram) {
88        self.client.deploy_binary_program(&program);
89    }
90
91    /// Warps the blockchain clock to a specific epoch
92    ///
93    /// This method updates the system clock sysvar to simulate time progression
94    /// to the specified epoch, useful for testing time-dependent program logic.
95    ///
96    /// # Arguments
97    /// * `warp_epoch` - The target epoch to warp to
98    pub fn warp_to_epoch(&mut self, warp_epoch: u64) {
99        let mut clock = self.get_sysvar::<Clock>();
100
101        clock.epoch = warp_epoch;
102        self.client.set_sysvar(&clock);
103    }
104
105    /// Warps the blockchain clock to a specific slot
106    ///
107    /// This method updates the system clock sysvar to simulate progression
108    /// to the specified slot number.
109    ///
110    /// # Arguments
111    /// * `warp_slot` - The target slot number to warp to
112    pub fn warp_to_slot(&mut self, warp_slot: u64) {
113        let mut clock = self.get_sysvar::<Clock>();
114
115        clock.slot = warp_slot;
116        self.client.set_sysvar(&clock);
117    }
118    /// Warps the blockchain clock to a specific Unix timestamp
119    ///
120    /// This method updates the system clock sysvar to simulate time progression
121    /// to the specified Unix timestamp.
122    ///
123    /// # Arguments
124    /// * `warp_timestamp` - The target Unix timestamp to warp to
125    pub fn warp_to_timestamp(&mut self, warp_timestamp: i64) {
126        let mut clock = self.get_sysvar::<Clock>();
127
128        clock.unix_timestamp = warp_timestamp;
129        self.client.set_sysvar(&clock);
130    }
131
132    /// Advances the blockchain clock by a specified number of seconds
133    ///
134    /// This method increments the current Unix timestamp by the given number
135    /// of seconds, useful for testing time-based program behavior.
136    ///
137    /// # Arguments
138    /// * `seconds` - The number of seconds to advance the clock
139    pub fn forward_in_time(&mut self, seconds: i64) {
140        let mut clock = self.get_sysvar::<Clock>();
141
142        clock.unix_timestamp = clock.unix_timestamp.saturating_add(seconds);
143        self.client.set_sysvar(&clock);
144    }
145
146    /// Sets a custom account state at the specified address
147    ///
148    /// This method allows you to manually set account data, lamports, and owner
149    /// for any public key, useful for setting up test scenarios.
150    ///
151    /// # Arguments
152    /// * `address` - The public key where the account should be stored
153    /// * `account` - The account data to set
154    pub fn set_account_custom(&mut self, address: &Pubkey, account: &AccountSharedData) {
155        self.client.set_account(address, account, false);
156    }
157
158    /// Returns the default payer keypair for transactions
159    ///
160    /// This keypair is used to pay transaction fees and sign transactions
161    /// when no other payer is specified.
162    ///
163    /// # Returns
164    /// The default payer keypair
165    pub fn payer(&self) -> solana_sdk::signature::Keypair {
166        self.client.get_payer()
167    }
168
169    /// Retrieves account data for the specified public key
170    ///
171    /// Returns the account data including lamports, owner, and data bytes.
172    /// If the account doesn't exist, returns a default empty account.
173    ///
174    /// # Arguments
175    /// * `key` - The public key of the account to retrieve
176    ///
177    /// # Returns
178    /// The account data or a default account if not found
179    pub fn get_account(&mut self, key: &Pubkey) -> AccountSharedData {
180        trident_svm::trident_svm::TridentSVM::get_account(&self.client, key).unwrap_or_default()
181    }
182    /// Retrieves and deserializes account data as a specific type
183    ///
184    /// This method fetches account data and attempts to deserialize it using Borsh,
185    /// skipping the specified discriminator bytes at the beginning.
186    ///
187    /// # Arguments
188    /// * `key` - The public key of the account to retrieve
189    /// * `discriminator_size` - Number of bytes to skip before deserializing
190    ///
191    /// # Returns
192    /// Some(T) if deserialization succeeds, None otherwise
193    pub fn get_account_with_type<T: BorshDeserialize>(
194        &mut self,
195        key: &Pubkey,
196        discriminator_size: usize,
197    ) -> Option<T> {
198        let account = self.get_account(key);
199        let data = account.data();
200
201        if data.len() > discriminator_size {
202            T::deserialize(&mut &data[discriminator_size..]).ok()
203        } else {
204            None
205        }
206    }
207
208    /// Gets the current Unix timestamp from the blockchain clock
209    ///
210    /// Returns the current timestamp as stored in the Clock sysvar.
211    ///
212    /// # Returns
213    /// The current Unix timestamp in seconds
214    pub fn get_current_timestamp(&self) -> i64 {
215        self.get_sysvar::<Clock>().unix_timestamp
216    }
217
218    /// Gets the last blockhash (not implemented for TridentSVM)
219    ///
220    /// # Panics
221    /// This method always panics as it's not yet implemented for TridentSVM
222    pub fn get_last_blockhash(&self) -> Hash {
223        panic!("Not yet implemented for TridentSVM");
224    }
225
226    fn process_instructions(
227        &mut self,
228        instructions: &[Instruction],
229    ) -> TridentTransactionProcessingResult {
230        // there should be at least 1 RW fee-payer account.
231        // But we do not pay for TX currently so has to be manually updated
232        // tx.message.header.num_required_signatures = 1;
233        // tx.message.header.num_readonly_signed_accounts = 0;
234        let tx = solana_sdk::transaction::Transaction::new_with_payer(
235            instructions,
236            Some(&self.payer().pubkey()),
237        );
238
239        self.client.process_transaction_with_settle(tx)
240    }
241
242    /// Retrieves a system variable (sysvar) of the specified type
243    ///
244    /// System variables contain blockchain state information like clock,
245    /// rent, and epoch schedule data.
246    ///
247    /// # Returns
248    /// The requested sysvar data
249    pub fn get_sysvar<T: Sysvar>(&self) -> T {
250        trident_svm::trident_svm::TridentSVM::get_sysvar::<T>(&self.client)
251    }
252
253    /// Airdrops SOL to the specified address
254    ///
255    /// This method adds the specified amount of lamports to the target account.
256    /// If the account doesn't exist, it will be created with the airdropped amount.
257    ///
258    /// # Arguments
259    /// * `address` - The public key to receive the airdrop
260    /// * `amount` - The number of lamports to airdrop
261    pub fn airdrop(&mut self, address: &Pubkey, amount: u64) {
262        let mut account = self.get_account(address);
263
264        account.set_lamports(account.lamports() + amount);
265        self.set_account_custom(address, &account);
266    }
267
268    /// Derives the program data address for an upgradeable program
269    ///
270    /// This method finds the program data account address for an upgradeable BPF loader program
271    /// by deriving a Program Derived Address (PDA) using the program's address as a seed.
272    ///
273    /// # Arguments
274    /// * `program_address` - The public key of the upgradeable program
275    ///
276    /// # Returns
277    /// The derived program data address (PDA)
278    pub fn get_program_data_address_v3(&self, program_address: &Pubkey) -> Pubkey {
279        Pubkey::find_program_address(
280            &[program_address.as_ref()],
281            &solana_sdk::bpf_loader_upgradeable::ID,
282        )
283        .0
284    }
285
286    /// Creates a program address (PDA) from seeds and a program ID
287    ///
288    /// This method attempts to create a valid program-derived address using the provided
289    /// seeds and program ID. Unlike `find_program_address`, this does not search for a
290    /// valid bump seed and will return None if the provided seeds don't produce a valid PDA.
291    ///
292    /// # Arguments
293    /// * `seeds` - Array of seed byte slices used to derive the address
294    /// * `program_id` - The program ID to use for derivation
295    ///
296    /// # Returns
297    /// Some(Pubkey) if the seeds produce a valid PDA, None otherwise
298    ///
299    /// # Example
300    /// ```rust,ignore
301    /// let seeds = &[b"my-seed", &[bump_seed]];
302    /// if let Some(pda) = trident.create_program_address(seeds, &program_id) {
303    ///     println!("Created PDA: {}", pda);
304    /// }
305    /// ```
306    pub fn create_program_address(&self, seeds: &[&[u8]], program_id: &Pubkey) -> Option<Pubkey> {
307        Pubkey::create_program_address(seeds, program_id).ok()
308    }
309
310    /// Finds a valid program address (PDA) and its bump seed
311    ///
312    /// This method searches for a valid program-derived address by trying different bump
313    /// seeds (starting from 255 and counting down) until a valid PDA is found. This is the
314    /// canonical way to derive PDAs in Solana programs.
315    ///
316    /// # Arguments
317    /// * `seeds` - Array of seed byte slices used to derive the address
318    /// * `program_id` - The program ID to use for derivation
319    ///
320    /// # Returns
321    /// A tuple containing the derived PDA and the bump seed used to generate it
322    ///
323    /// # Example
324    /// ```rust,ignore
325    /// let seeds = &[b"my-seed", user_pubkey.as_ref()];
326    /// let (pda, bump) = trident.find_program_address(seeds, &program_id);
327    /// println!("Found PDA: {} with bump: {}", pda, bump);
328    /// ```
329    pub fn find_program_address(&self, seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
330        Pubkey::find_program_address(seeds, program_id)
331    }
332
333    fn handle_tx_result(
334        &mut self,
335        tx_processing_result: &TridentTransactionProcessingResult,
336        log_as: Option<&str>,
337        instructions: &[Instruction],
338    ) -> TransactionResult {
339        let fuzzing_metrics = std::env::var("FUZZING_METRICS");
340        let fuzzing_debug = std::env::var("TRIDENT_FUZZ_DEBUG");
341
342        // NOTE: for now we just expect that one transaction was executed
343        let tx_result = &tx_processing_result.get_result().processing_results[0];
344
345        let transaction_timestamp = tx_processing_result.get_transaction_timestamp();
346
347        match tx_result {
348            Ok(result) => match result {
349                trident_svm::prelude::solana_svm::transaction_processing_result::ProcessedTransaction::Executed(executed_transaction) => match &executed_transaction.execution_details.status {
350                    Ok(_) => {
351                        // Record successful execution
352                        if fuzzing_metrics.is_ok() && log_as.is_some() {
353                            if let Some(log_as) = log_as {
354                                self.fuzzing_data
355                                    .add_successful_transaction(log_as);
356                            }
357                        }
358                        TransactionResult::new(Ok(()), executed_transaction.execution_details.log_messages.clone().unwrap_or_default(), transaction_timestamp)
359                    },
360                    Err(transaction_error) => {
361                        if let TransactionError::InstructionError(_error_code, instruction_error) =
362                            &transaction_error
363                        {
364                            match instruction_error {
365                                InstructionError::ProgramFailedToComplete => {
366                                    if fuzzing_metrics.is_ok() {
367                                        if fuzzing_debug.is_ok() {
368                                            trident_svm::prelude::trident_svm_log::log_message(
369                                                "TRANSACTION PANICKED",
370                                                trident_svm::prelude::Level::Error,
371                                            );
372                                        }
373                                        if log_as.is_some() {
374                                            let rng = self.rng.get_seed();
375                                            // TODO format instructions
376                                            let tx = format!("{:#?}", instructions);
377                                            self.fuzzing_data.add_transaction_panicked(
378                                                log_as.unwrap(),
379                                                rng,
380                                                instruction_error.to_string(),
381                                                executed_transaction.execution_details.log_messages.clone(),
382                                                tx,
383                                            );
384                                        }
385                                    }
386                                }
387                                InstructionError::Custom(error_code) => {
388                                    if fuzzing_metrics.is_ok() && log_as.is_some() {
389                                        if let Some(log_as) = log_as {
390                                            self.fuzzing_data.add_custom_instruction_error(
391                                                log_as,
392                                                error_code,
393                                                executed_transaction.execution_details.log_messages.clone(),
394                                            );
395                                        }
396                                    }
397                                }
398                                _ => {
399                                    if fuzzing_metrics.is_ok() && log_as.is_some() {
400                                        if let Some(log_as) = log_as {
401                                            self.fuzzing_data.add_failed_transaction(
402                                                log_as,
403                                                    instruction_error.to_string(),
404                                                    executed_transaction.execution_details.log_messages.clone(),
405                                                );
406                                        }
407                                    }
408                                }
409                            }
410                        } else if fuzzing_metrics.is_ok() && log_as.is_some() {
411                            if let Some(log_as) = log_as {
412                            self.fuzzing_data.add_failed_transaction(
413                                log_as,
414                                    transaction_error.to_string(),
415                                    executed_transaction.execution_details.log_messages.clone(),
416                                );
417                            }
418                        }
419                        TransactionResult::new(Err(transaction_error.clone()), executed_transaction.execution_details.log_messages.clone().unwrap_or_default(), transaction_timestamp)
420                    },
421                },
422                trident_svm::prelude::solana_svm::transaction_processing_result::ProcessedTransaction::FeesOnly(_) => todo!(),
423            },
424            Err(transaction_error) => TransactionResult::new(Err(transaction_error.clone()), vec![], transaction_timestamp),
425        }
426    }
427}