light_program_test/program_test/
light_program_test.rs1use std::fmt::{self, Debug, Formatter};
2
3#[cfg(feature = "devenv")]
4use account_compression::QueueAccount;
5use light_client::{
6 indexer::{AddressMerkleTreeAccounts, StateMerkleTreeAccounts},
7 rpc::{merkle_tree::MerkleTreeExt, RpcError},
8};
9#[cfg(feature = "devenv")]
10use light_compressed_account::hash_to_bn254_field_size_be;
11use light_prover_client::prover::spawn_prover;
12use litesvm::LiteSVM;
13#[cfg(feature = "devenv")]
14use solana_account::WritableAccount;
15use solana_sdk::signature::{Keypair, Signer};
16
17#[cfg(feature = "devenv")]
18use crate::accounts::initialize::initialize_accounts;
19#[cfg(feature = "devenv")]
20use crate::program_test::TestRpc;
21use crate::{
22 accounts::{test_accounts::TestAccounts, test_keypairs::TestKeypairs},
23 indexer::TestIndexer,
24 utils::setup_light_programs::setup_light_programs,
25 ProgramTestConfig,
26};
27
28pub struct LightProgramTest {
29 pub config: ProgramTestConfig,
30 pub context: LiteSVM,
31 pub pre_context: Option<LiteSVM>,
32 pub indexer: Option<TestIndexer>,
33 pub test_accounts: TestAccounts,
34 pub payer: Keypair,
35 pub transaction_counter: usize,
36 pub auto_mine_cold_state_programs: Vec<solana_sdk::pubkey::Pubkey>,
37}
38
39impl LightProgramTest {
40 pub fn get_pre_transaction_account(
59 &self,
60 pubkey: &solana_sdk::pubkey::Pubkey,
61 ) -> Option<solana_sdk::account::Account> {
62 self.pre_context
63 .as_ref()
64 .and_then(|ctx| ctx.get_account(pubkey))
65 }
66
67 pub async fn new(config: ProgramTestConfig) -> Result<LightProgramTest, RpcError> {
68 let mut context = setup_light_programs(config.additional_programs.clone())?;
69 let payer = Keypair::new();
70 context
71 .airdrop(&payer.pubkey(), 100_000_000_000_000)
72 .expect("Payer airdrop failed.");
73 let mut context = Self {
74 context,
75 pre_context: None,
76 indexer: None,
77 test_accounts: TestAccounts::get_program_test_test_accounts(),
78 payer,
79 config: config.clone(),
80 transaction_counter: 0,
81 auto_mine_cold_state_programs: Vec::new(),
82 };
83 let keypairs = TestKeypairs::program_test_default();
84
85 context
86 .context
87 .airdrop(&keypairs.governance_authority.pubkey(), 100_000_000_000_000)
88 .expect("governance_authority airdrop failed.");
89 context
90 .context
91 .airdrop(&keypairs.forester.pubkey(), 10_000_000_000)
92 .expect("forester airdrop failed.");
93
94 #[cfg(feature = "devenv")]
95 {
96 if !config.skip_protocol_init {
97 let restore_logs = context.config.no_logs;
98 if context.config.skip_startup_logs {
99 context.config.no_logs = true;
100 }
101 initialize_accounts(&mut context, &config, &keypairs).await?;
102 crate::accounts::compressible_config::create_compressible_config(&mut context)
103 .await?;
104 if context.config.skip_startup_logs {
105 context.config.no_logs = restore_logs;
106 }
107 let batch_size = config
108 .v2_state_tree_config
109 .as_ref()
110 .map(|config| config.output_queue_batch_size as usize);
111 let test_accounts = context.test_accounts.clone();
112 context.add_indexer(&test_accounts, batch_size).await?;
113
114 {
116 use crate::utils::load_accounts::load_account_from_dir;
117
118 if context.test_accounts.v1_address_trees.len() != 1 {
119 return Err(RpcError::CustomError(format!(
120 "Expected exactly 1 V1 address tree, found {}. V1 address trees are deprecated and only one is supported.",
121 context.test_accounts.v1_address_trees.len()
122 )));
123 }
124
125 let address_mt = context.test_accounts.v1_address_trees[0].merkle_tree;
126 let address_queue_pubkey = context.test_accounts.v1_address_trees[0].queue;
127
128 let tree_account =
129 load_account_from_dir(&address_mt, Some("address_merkle_tree"))?;
130 context
131 .context
132 .set_account(address_mt, tree_account)
133 .map_err(|e| {
134 RpcError::CustomError(format!(
135 "Failed to set V1 address tree account: {}",
136 e
137 ))
138 })?;
139
140 let queue_account = load_account_from_dir(
141 &address_queue_pubkey,
142 Some("address_merkle_tree_queue"),
143 )?;
144 context
145 .context
146 .set_account(address_queue_pubkey, queue_account)
147 .map_err(|e| {
148 RpcError::CustomError(format!(
149 "Failed to set V1 address queue account: {}",
150 e
151 ))
152 })?;
153 }
154 }
155 let (auto_register, additional_programs) = {
156 let auto = context
157 .config
158 .auto_register_custom_programs_for_pda_compression;
159 let progs = context.config.additional_programs.clone();
160 (auto, progs)
161 };
162 if auto_register {
163 if let Some(programs) = additional_programs {
164 for (_, pid) in programs.into_iter() {
165 if !context.auto_mine_cold_state_programs.contains(&pid) {
166 context.auto_mine_cold_state_programs.push(pid);
167 }
168 }
169 }
170 }
171 {
173 let tree_account = context
174 .context
175 .get_account(&keypairs.state_merkle_tree.pubkey());
176 let queue_account = context
177 .context
178 .get_account(&keypairs.nullifier_queue.pubkey());
179 let cpi_account = context
180 .context
181 .get_account(&keypairs.cpi_context_account.pubkey());
182
183 if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
184 (tree_account, queue_account, cpi_account)
185 {
186 for i in 0..context.test_accounts.v1_state_trees.len() {
187 let state_mt = context.test_accounts.v1_state_trees[i].merkle_tree;
188 let nullifier_queue_pubkey =
189 context.test_accounts.v1_state_trees[i].nullifier_queue;
190 let cpi_context_pubkey =
191 context.test_accounts.v1_state_trees[i].cpi_context;
192
193 let mut tree_account_data = tree_acc.clone();
195 {
196 let merkle_tree_account = bytemuck::from_bytes_mut::<
197 account_compression::StateMerkleTreeAccount,
198 >(
199 &mut tree_account_data.data_as_mut_slice()
200 [8..account_compression::StateMerkleTreeAccount::LEN],
201 );
202 merkle_tree_account.metadata.associated_queue =
203 nullifier_queue_pubkey.into();
204 }
205 context.set_account(state_mt, tree_account_data);
206
207 let mut queue_account_data = queue_acc.clone();
209 {
210 let queue_account = bytemuck::from_bytes_mut::<QueueAccount>(
211 &mut queue_account_data.data_as_mut_slice()[8..QueueAccount::LEN],
212 );
213 queue_account.metadata.associated_merkle_tree = state_mt.into();
214 }
215 context.set_account(nullifier_queue_pubkey, queue_account_data);
216
217 let mut cpi_account_data = cpi_acc.clone();
219 {
220 let associated_merkle_tree_offset = 8 + 32; let associated_queue_offset = 8 + 32 + 32; cpi_account_data.data_as_mut_slice()
223 [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
224 .copy_from_slice(&state_mt.to_bytes());
225 cpi_account_data.data_as_mut_slice()
226 [associated_queue_offset..associated_queue_offset + 32]
227 .copy_from_slice(&nullifier_queue_pubkey.to_bytes());
228 }
229 context.set_account(cpi_context_pubkey, cpi_account_data);
230 }
231 }
232 }
233 {
234 let address_mt = context.test_accounts.v2_address_trees[0];
235 let account = context
236 .context
237 .get_account(&keypairs.batch_address_merkle_tree.pubkey());
238 if let Some(account) = account {
239 context.set_account(address_mt, account);
240 }
241 }
242 {
244 let tree_account = context
245 .context
246 .get_account(&keypairs.batched_state_merkle_tree.pubkey());
247 let queue_account = context
248 .context
249 .get_account(&keypairs.batched_output_queue.pubkey());
250 let cpi_account = context
251 .context
252 .get_account(&keypairs.batched_cpi_context.pubkey());
253
254 if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
255 (tree_account, queue_account, cpi_account)
256 {
257 use light_batched_merkle_tree::{
258 merkle_tree::BatchedMerkleTreeAccount, queue::BatchedQueueAccount,
259 };
260
261 for i in 0..context.test_accounts.v2_state_trees.len() {
262 let merkle_tree_pubkey =
263 context.test_accounts.v2_state_trees[i].merkle_tree;
264 let output_queue_pubkey =
265 context.test_accounts.v2_state_trees[i].output_queue;
266 let cpi_context_pubkey =
267 context.test_accounts.v2_state_trees[i].cpi_context;
268
269 let mut tree_account_data = tree_acc.clone();
271 {
272 let mut tree = BatchedMerkleTreeAccount::state_from_bytes(
273 tree_account_data.data_as_mut_slice(),
274 &merkle_tree_pubkey.into(),
275 )
276 .unwrap();
277 let metadata = tree.get_metadata_mut();
278 metadata.metadata.associated_queue = output_queue_pubkey.into();
279 metadata.hashed_pubkey =
280 hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
281 }
282 context.set_account(merkle_tree_pubkey, tree_account_data);
283
284 let mut queue_account_data = queue_acc.clone();
286 {
287 let mut queue = BatchedQueueAccount::output_from_bytes(
288 queue_account_data.data_as_mut_slice(),
289 )
290 .unwrap();
291 let metadata = queue.get_metadata_mut();
292 metadata.metadata.associated_merkle_tree = merkle_tree_pubkey.into();
293 metadata.hashed_merkle_tree_pubkey =
294 hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
295 metadata.hashed_queue_pubkey =
296 hash_to_bn254_field_size_be(&output_queue_pubkey.to_bytes());
297 }
298 context.set_account(output_queue_pubkey, queue_account_data);
299
300 let mut cpi_account_data = cpi_acc.clone();
302 {
303 let associated_merkle_tree_offset = 8 + 32; let associated_queue_offset = 8 + 32 + 32; cpi_account_data.data_as_mut_slice()
306 [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
307 .copy_from_slice(&merkle_tree_pubkey.to_bytes());
308 cpi_account_data.data_as_mut_slice()
309 [associated_queue_offset..associated_queue_offset + 32]
310 .copy_from_slice(&output_queue_pubkey.to_bytes());
311 }
312 context.set_account(cpi_context_pubkey, cpi_account_data);
313 }
314 }
315 }
316 }
317
318 #[cfg(not(feature = "devenv"))]
319 {
320 use crate::utils::load_accounts::load_all_accounts_from_dir;
322
323 let accounts = load_all_accounts_from_dir()?;
324
325 const BATCH_SIZE_OFFSET: usize = 240;
328 let mut batch_sizes = Vec::new();
329
330 for v2_tree in &context.test_accounts.v2_state_trees {
331 if let Some(queue_account) = accounts.get(&v2_tree.output_queue) {
332 if queue_account.data.len() >= BATCH_SIZE_OFFSET + 8 {
333 let bytes: [u8; 8] = queue_account.data
334 [BATCH_SIZE_OFFSET..BATCH_SIZE_OFFSET + 8]
335 .try_into()
336 .map_err(|_| {
337 RpcError::CustomError("Failed to read batch_size bytes".to_string())
338 })?;
339 batch_sizes.push(u64::from_le_bytes(bytes) as usize);
340 }
341 }
342 }
343
344 if !batch_sizes.is_empty() && !batch_sizes.windows(2).all(|w| w[0] == w[1]) {
346 return Err(RpcError::CustomError(format!(
347 "Inconsistent batch_sizes found across output queues: {:?}",
348 batch_sizes
349 )));
350 }
351
352 let batch_size = batch_sizes.first().copied().unwrap_or(0);
353
354 for (pubkey, account) in accounts {
355 context.context.set_account(pubkey, account).map_err(|e| {
356 RpcError::CustomError(format!("Failed to set account {}: {}", pubkey, e))
357 })?;
358 }
359
360 crate::registry_sdk::setup_test_protocol_accounts(
363 &mut context.context,
364 &keypairs.forester.pubkey(),
365 )
366 .map_err(|e| RpcError::CustomError(e))?;
367
368 let test_accounts = context.test_accounts.clone();
370 context
371 .add_indexer(&test_accounts, Some(batch_size))
372 .await?;
373
374 if let Some(programs) = context.config.additional_programs.clone() {
377 for (_, pid) in programs.into_iter() {
378 if !context.auto_mine_cold_state_programs.contains(&pid) {
379 context.auto_mine_cold_state_programs.push(pid);
380 }
381 }
382 }
383 }
384
385 context.transaction_counter = 0;
387
388 #[cfg(feature = "devenv")]
389 {
390 spawn_prover().await;
391 }
392 #[cfg(not(feature = "devenv"))]
393 if config.with_prover {
394 spawn_prover().await;
395 }
396
397 Ok(context)
398 }
399
400 pub fn indexer(&self) -> Result<&TestIndexer, RpcError> {
401 self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
402 }
403
404 pub fn indexer_mut(&mut self) -> Result<&mut TestIndexer, RpcError> {
405 self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
406 }
407
408 pub fn test_accounts(&self) -> &TestAccounts {
409 &self.test_accounts
410 }
411
412 pub fn get_state_merkle_tree_account(&self) -> StateMerkleTreeAccounts {
414 self.test_accounts.v1_state_trees[0]
415 }
416
417 pub fn get_address_merkle_tree(&self) -> AddressMerkleTreeAccounts {
418 self.test_accounts.v1_address_trees[0]
419 }
420
421 pub async fn add_indexer(
422 &mut self,
423 test_accounts: &TestAccounts,
424 batch_size: Option<usize>,
425 ) -> Result<(), RpcError> {
426 let indexer = TestIndexer::init_from_acounts(
427 &self.payer,
428 test_accounts,
429 batch_size.unwrap_or_default(),
430 )
431 .await;
432 self.indexer = Some(indexer);
433 Ok(())
434 }
435
436 pub fn clone_indexer(&self) -> Result<TestIndexer, RpcError> {
437 Ok((*self
438 .indexer
439 .as_ref()
440 .ok_or(RpcError::IndexerNotInitialized)?)
441 .clone())
442 }
443
444 #[cfg(feature = "devenv")]
445 pub fn disable_cold_state_mining(&mut self, program_id: solana_sdk::pubkey::Pubkey) {
446 self.auto_mine_cold_state_programs
447 .retain(|&pid| pid != program_id);
448 }
449}
450
451impl MerkleTreeExt for LightProgramTest {}
452
453impl Debug for LightProgramTest {
454 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
455 f.debug_struct("LightProgramTest")
456 .field("context", &"ProgramTestContext")
457 .field("indexer", &self.indexer)
458 .field("test_accounts", &self.test_accounts)
459 .finish()
460 }
461}