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 let (rent_sponsor, _) = light_account::derive_rent_sponsor_pda(&pid);
170 context
171 .context
172 .airdrop(&rent_sponsor, 100_000_000_000)
173 .expect("rent_sponsor airdrop failed.");
174 }
175 }
176 }
177 {
179 let tree_account = context
180 .context
181 .get_account(&keypairs.state_merkle_tree.pubkey());
182 let queue_account = context
183 .context
184 .get_account(&keypairs.nullifier_queue.pubkey());
185 let cpi_account = context
186 .context
187 .get_account(&keypairs.cpi_context_account.pubkey());
188
189 if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
190 (tree_account, queue_account, cpi_account)
191 {
192 for i in 0..context.test_accounts.v1_state_trees.len() {
193 let state_mt = context.test_accounts.v1_state_trees[i].merkle_tree;
194 let nullifier_queue_pubkey =
195 context.test_accounts.v1_state_trees[i].nullifier_queue;
196 let cpi_context_pubkey =
197 context.test_accounts.v1_state_trees[i].cpi_context;
198
199 let mut tree_account_data = tree_acc.clone();
201 {
202 let merkle_tree_account = bytemuck::from_bytes_mut::<
203 account_compression::StateMerkleTreeAccount,
204 >(
205 &mut tree_account_data.data_as_mut_slice()
206 [8..account_compression::StateMerkleTreeAccount::LEN],
207 );
208 merkle_tree_account.metadata.associated_queue =
209 nullifier_queue_pubkey.into();
210 }
211 context.set_account(state_mt, tree_account_data);
212
213 let mut queue_account_data = queue_acc.clone();
215 {
216 let queue_account = bytemuck::from_bytes_mut::<QueueAccount>(
217 &mut queue_account_data.data_as_mut_slice()[8..QueueAccount::LEN],
218 );
219 queue_account.metadata.associated_merkle_tree = state_mt.into();
220 }
221 context.set_account(nullifier_queue_pubkey, queue_account_data);
222
223 let mut cpi_account_data = cpi_acc.clone();
225 {
226 let associated_merkle_tree_offset = 8 + 32; let associated_queue_offset = 8 + 32 + 32; cpi_account_data.data_as_mut_slice()
229 [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
230 .copy_from_slice(&state_mt.to_bytes());
231 cpi_account_data.data_as_mut_slice()
232 [associated_queue_offset..associated_queue_offset + 32]
233 .copy_from_slice(&nullifier_queue_pubkey.to_bytes());
234 }
235 context.set_account(cpi_context_pubkey, cpi_account_data);
236 }
237 }
238 }
239 {
240 let address_mt = context.test_accounts.v2_address_trees[0];
241 let account = context
242 .context
243 .get_account(&keypairs.batch_address_merkle_tree.pubkey());
244 if let Some(account) = account {
245 context.set_account(address_mt, account);
246 }
247 }
248 {
250 let tree_account = context
251 .context
252 .get_account(&keypairs.batched_state_merkle_tree.pubkey());
253 let queue_account = context
254 .context
255 .get_account(&keypairs.batched_output_queue.pubkey());
256 let cpi_account = context
257 .context
258 .get_account(&keypairs.batched_cpi_context.pubkey());
259
260 if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
261 (tree_account, queue_account, cpi_account)
262 {
263 use light_batched_merkle_tree::{
264 merkle_tree::BatchedMerkleTreeAccount, queue::BatchedQueueAccount,
265 };
266
267 for i in 0..context.test_accounts.v2_state_trees.len() {
268 let merkle_tree_pubkey =
269 context.test_accounts.v2_state_trees[i].merkle_tree;
270 let output_queue_pubkey =
271 context.test_accounts.v2_state_trees[i].output_queue;
272 let cpi_context_pubkey =
273 context.test_accounts.v2_state_trees[i].cpi_context;
274
275 let mut tree_account_data = tree_acc.clone();
277 {
278 let mut tree = BatchedMerkleTreeAccount::state_from_bytes(
279 tree_account_data.data_as_mut_slice(),
280 &merkle_tree_pubkey.into(),
281 )
282 .unwrap();
283 let metadata = tree.get_metadata_mut();
284 metadata.metadata.associated_queue = output_queue_pubkey.into();
285 metadata.hashed_pubkey =
286 hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
287 }
288 context.set_account(merkle_tree_pubkey, tree_account_data);
289
290 let mut queue_account_data = queue_acc.clone();
292 {
293 let mut queue = BatchedQueueAccount::output_from_bytes(
294 queue_account_data.data_as_mut_slice(),
295 )
296 .unwrap();
297 let metadata = queue.get_metadata_mut();
298 metadata.metadata.associated_merkle_tree = merkle_tree_pubkey.into();
299 metadata.hashed_merkle_tree_pubkey =
300 hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
301 metadata.hashed_queue_pubkey =
302 hash_to_bn254_field_size_be(&output_queue_pubkey.to_bytes());
303 }
304 context.set_account(output_queue_pubkey, queue_account_data);
305
306 let mut cpi_account_data = cpi_acc.clone();
308 {
309 let associated_merkle_tree_offset = 8 + 32; let associated_queue_offset = 8 + 32 + 32; cpi_account_data.data_as_mut_slice()
312 [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
313 .copy_from_slice(&merkle_tree_pubkey.to_bytes());
314 cpi_account_data.data_as_mut_slice()
315 [associated_queue_offset..associated_queue_offset + 32]
316 .copy_from_slice(&output_queue_pubkey.to_bytes());
317 }
318 context.set_account(cpi_context_pubkey, cpi_account_data);
319 }
320 }
321 }
322 }
323
324 #[cfg(not(feature = "devenv"))]
325 {
326 use crate::utils::load_accounts::load_all_accounts_from_dir;
328
329 let accounts = load_all_accounts_from_dir()?;
330
331 const BATCH_SIZE_OFFSET: usize = 240;
334 let mut batch_sizes = Vec::new();
335
336 for v2_tree in &context.test_accounts.v2_state_trees {
337 if let Some(queue_account) = accounts.get(&v2_tree.output_queue) {
338 if queue_account.data.len() >= BATCH_SIZE_OFFSET + 8 {
339 let bytes: [u8; 8] = queue_account.data
340 [BATCH_SIZE_OFFSET..BATCH_SIZE_OFFSET + 8]
341 .try_into()
342 .map_err(|_| {
343 RpcError::CustomError("Failed to read batch_size bytes".to_string())
344 })?;
345 batch_sizes.push(u64::from_le_bytes(bytes) as usize);
346 }
347 }
348 }
349
350 if !batch_sizes.is_empty() && !batch_sizes.windows(2).all(|w| w[0] == w[1]) {
352 return Err(RpcError::CustomError(format!(
353 "Inconsistent batch_sizes found across output queues: {:?}",
354 batch_sizes
355 )));
356 }
357
358 let batch_size = batch_sizes.first().copied().unwrap_or(0);
359
360 for (pubkey, account) in accounts {
361 context.context.set_account(pubkey, account).map_err(|e| {
362 RpcError::CustomError(format!("Failed to set account {}: {}", pubkey, e))
363 })?;
364 }
365
366 crate::registry_sdk::setup_test_protocol_accounts(
369 &mut context.context,
370 &keypairs.forester.pubkey(),
371 )
372 .map_err(|e| RpcError::CustomError(e))?;
373
374 let test_accounts = context.test_accounts.clone();
376 context
377 .add_indexer(&test_accounts, Some(batch_size))
378 .await?;
379
380 if let Some(programs) = context.config.additional_programs.clone() {
383 for (_, pid) in programs.into_iter() {
384 if !context.auto_mine_cold_state_programs.contains(&pid) {
385 context.auto_mine_cold_state_programs.push(pid);
386 }
387 }
388 }
389 }
390
391 context.transaction_counter = 0;
393
394 #[cfg(feature = "devenv")]
395 {
396 spawn_prover().await;
397 }
398 #[cfg(not(feature = "devenv"))]
399 if config.with_prover {
400 spawn_prover().await;
401 }
402
403 Ok(context)
404 }
405
406 pub fn indexer(&self) -> Result<&TestIndexer, RpcError> {
407 self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
408 }
409
410 pub fn indexer_mut(&mut self) -> Result<&mut TestIndexer, RpcError> {
411 self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
412 }
413
414 pub fn test_accounts(&self) -> &TestAccounts {
415 &self.test_accounts
416 }
417
418 pub fn get_state_merkle_tree_account(&self) -> StateMerkleTreeAccounts {
420 self.test_accounts.v1_state_trees[0]
421 }
422
423 pub fn get_address_merkle_tree(&self) -> AddressMerkleTreeAccounts {
424 self.test_accounts.v1_address_trees[0]
425 }
426
427 pub async fn add_indexer(
428 &mut self,
429 test_accounts: &TestAccounts,
430 batch_size: Option<usize>,
431 ) -> Result<(), RpcError> {
432 let indexer = TestIndexer::init_from_acounts(
433 &self.payer,
434 test_accounts,
435 batch_size.unwrap_or_default(),
436 )
437 .await;
438 self.indexer = Some(indexer);
439 Ok(())
440 }
441
442 pub fn clone_indexer(&self) -> Result<TestIndexer, RpcError> {
443 Ok((*self
444 .indexer
445 .as_ref()
446 .ok_or(RpcError::IndexerNotInitialized)?)
447 .clone())
448 }
449
450 #[cfg(feature = "devenv")]
451 pub fn disable_cold_state_mining(&mut self, program_id: solana_sdk::pubkey::Pubkey) {
452 self.auto_mine_cold_state_programs
453 .retain(|&pid| pid != program_id);
454 }
455}
456
457impl MerkleTreeExt for LightProgramTest {}
458
459impl Debug for LightProgramTest {
460 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
461 f.debug_struct("LightProgramTest")
462 .field("context", &"ProgramTestContext")
463 .field("indexer", &self.indexer)
464 .field("test_accounts", &self.test_accounts)
465 .finish()
466 }
467}