Skip to main content

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                        // Airdrop to program's rent sponsor PDA for decompression
169                        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            // Copy v1 state merkle tree accounts to devnet pubkeys
178            {
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                        // Update tree account with correct associated queue
200                        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                        // Update queue account with correct associated merkle tree
214                        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                        // Update CPI context account with correct associated merkle tree and queue
224                        let mut cpi_account_data = cpi_acc.clone();
225                        {
226                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
227                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
228                            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            // Copy batched state merkle tree accounts to devnet pubkeys
249            {
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                        // Update tree account with correct associated queue and hashed pubkey
276                        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                        // Update queue account with correct associated merkle tree and hashed pubkeys
291                        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                        // Update CPI context account with correct associated merkle tree and queue
307                        let mut cpi_account_data = cpi_acc.clone();
308                        {
309                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
310                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
311                            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            // Load all accounts from JSON directory
327            use crate::utils::load_accounts::load_all_accounts_from_dir;
328
329            let accounts = load_all_accounts_from_dir()?;
330
331            // Extract and verify batch_size from all V2 state tree output queues
332            // BatchedQueueMetadata layout: discriminator (8) + QueueMetadata (224) + QueueBatches.num_batches (8) + QueueBatches.batch_size (8)
333            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            // Verify all batch sizes are the same
351            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            // Set up protocol config and forester accounts for compress/close operations
367            // This must come AFTER loading JSON accounts to avoid being overwritten
368            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            // Initialize indexer with extracted batch size
375            let test_accounts = context.test_accounts.clone();
376            context
377                .add_indexer(&test_accounts, Some(batch_size))
378                .await?;
379
380            // Register additional programs for auto-compression of their PDAs
381            // In non-devenv mode, always register since we can't configure otherwise
382            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        // reset tx counter after program setup.
392        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    /// Get account pubkeys of one state Merkle tree.
419    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}