light_program_test/program_test/
light_program_test.rs

1use 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    /// Creates ProgramTestContext with light protocol and additional programs.
41    ///
42    /// Programs:
43    /// 1. light program
44    /// 2. account_compression program
45    /// 3. light_compressed_token program
46    /// 4. light_system_program program
47    ///
48    /// Light Protocol accounts:
49    /// 5. creates and initializes governance authority
50    /// 6. creates and initializes group authority
51    /// 7. registers the light_system_program program with the group authority
52    /// 8. initializes Merkle tree owned by
53    /// Note:
54    /// - registers a forester
55    /// - advances to the active phase slot 2
56    /// - active phase doesn't end
57    ///   Get an account from the pre-transaction context (before the last transaction)
58    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                // Load V1 address tree accounts from JSON files
115                {
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            // Copy v1 state merkle tree accounts to devnet pubkeys
172            {
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                        // Update tree account with correct associated queue
194                        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                        // Update queue account with correct associated merkle tree
208                        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                        // Update CPI context account with correct associated merkle tree and queue
218                        let mut cpi_account_data = cpi_acc.clone();
219                        {
220                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
221                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
222                            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            // Copy batched state merkle tree accounts to devnet pubkeys
243            {
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                        // Update tree account with correct associated queue and hashed pubkey
270                        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                        // Update queue account with correct associated merkle tree and hashed pubkeys
285                        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                        // Update CPI context account with correct associated merkle tree and queue
301                        let mut cpi_account_data = cpi_acc.clone();
302                        {
303                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
304                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
305                            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            // Load all accounts from JSON directory
321            use crate::utils::load_accounts::load_all_accounts_from_dir;
322
323            let accounts = load_all_accounts_from_dir()?;
324
325            // Extract and verify batch_size from all V2 state tree output queues
326            // BatchedQueueMetadata layout: discriminator (8) + QueueMetadata (224) + QueueBatches.num_batches (8) + QueueBatches.batch_size (8)
327            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            // Verify all batch sizes are the same
345            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            // Set up protocol config and forester accounts for compress/close operations
361            // This must come AFTER loading JSON accounts to avoid being overwritten
362            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            // Initialize indexer with extracted batch size
369            let test_accounts = context.test_accounts.clone();
370            context
371                .add_indexer(&test_accounts, Some(batch_size))
372                .await?;
373
374            // Register additional programs for auto-compression of their PDAs
375            // In non-devenv mode, always register since we can't configure otherwise
376            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        // reset tx counter after program setup.
386        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    /// Get account pubkeys of one state Merkle tree.
413    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}