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}