light_program_test/program_test/
light_program_test.rs

1use std::fmt::{self, Debug, Formatter};
2
3#[cfg(feature = "devenv")]
4use account_compression::{AddressMerkleTreeAccount, 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}
37
38impl LightProgramTest {
39    /// Creates ProgramTestContext with light protocol and additional programs.
40    ///
41    /// Programs:
42    /// 1. light program
43    /// 2. account_compression program
44    /// 3. light_compressed_token program
45    /// 4. light_system_program program
46    ///
47    /// Light Protocol accounts:
48    /// 5. creates and initializes governance authority
49    /// 6. creates and initializes group authority
50    /// 7. registers the light_system_program program with the group authority
51    /// 8. initializes Merkle tree owned by
52    /// Note:
53    /// - registers a forester
54    /// - advances to the active phase slot 2
55    /// - active phase doesn't end
56    ///   Get an account from the pre-transaction context (before the last transaction)
57    pub fn get_pre_transaction_account(
58        &self,
59        pubkey: &solana_sdk::pubkey::Pubkey,
60    ) -> Option<solana_sdk::account::Account> {
61        self.pre_context
62            .as_ref()
63            .and_then(|ctx| ctx.get_account(pubkey))
64    }
65
66    pub async fn new(config: ProgramTestConfig) -> Result<LightProgramTest, RpcError> {
67        let mut context = setup_light_programs(config.additional_programs.clone())?;
68        let payer = Keypair::new();
69        context
70            .airdrop(&payer.pubkey(), 100_000_000_000_000)
71            .expect("Payer airdrop failed.");
72        let mut context = Self {
73            context,
74            pre_context: None,
75            indexer: None,
76            test_accounts: TestAccounts::get_program_test_test_accounts(),
77            payer,
78            config: config.clone(),
79            transaction_counter: 0,
80        };
81        let keypairs = TestKeypairs::program_test_default();
82
83        context
84            .context
85            .airdrop(&keypairs.governance_authority.pubkey(), 100_000_000_000_000)
86            .expect("governance_authority airdrop failed.");
87        context
88            .context
89            .airdrop(&keypairs.forester.pubkey(), 10_000_000_000)
90            .expect("forester airdrop failed.");
91
92        #[cfg(feature = "devenv")]
93        {
94            if !config.skip_protocol_init {
95                let restore_logs = context.config.no_logs;
96                if context.config.skip_startup_logs {
97                    context.config.no_logs = true;
98                }
99                initialize_accounts(&mut context, &config, &keypairs).await?;
100                crate::accounts::compressible_config::create_compressible_config(&mut context)
101                    .await?;
102                if context.config.skip_startup_logs {
103                    context.config.no_logs = restore_logs;
104                }
105                let batch_size = config
106                    .v2_state_tree_config
107                    .as_ref()
108                    .map(|config| config.output_queue_batch_size as usize);
109                let test_accounts = context.test_accounts.clone();
110                context.add_indexer(&test_accounts, batch_size).await?;
111
112                // ensure that address tree pubkey is amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2
113                {
114                    let address_mt = context.test_accounts.v1_address_trees[0].merkle_tree;
115                    let address_queue_pubkey = context.test_accounts.v1_address_trees[0].queue;
116                    let mut account = context
117                        .context
118                        .get_account(&keypairs.address_merkle_tree.pubkey())
119                        .unwrap();
120                    let merkle_tree_account = bytemuck::from_bytes_mut::<AddressMerkleTreeAccount>(
121                        &mut account.data_as_mut_slice()[8..AddressMerkleTreeAccount::LEN],
122                    );
123                    merkle_tree_account.metadata.associated_queue = address_queue_pubkey.into();
124                    context.set_account(address_mt, account);
125
126                    let mut account = context
127                        .context
128                        .get_account(&keypairs.address_merkle_tree_queue.pubkey())
129                        .unwrap();
130                    let queue_account = bytemuck::from_bytes_mut::<QueueAccount>(
131                        &mut account.data_as_mut_slice()[8..QueueAccount::LEN],
132                    );
133                    queue_account.metadata.associated_merkle_tree = address_mt.into();
134                    context.set_account(address_queue_pubkey, account);
135                }
136            }
137            // Copy v1 state merkle tree accounts to devnet pubkeys
138            {
139                let tree_account = context
140                    .context
141                    .get_account(&keypairs.state_merkle_tree.pubkey());
142                let queue_account = context
143                    .context
144                    .get_account(&keypairs.nullifier_queue.pubkey());
145                let cpi_account = context
146                    .context
147                    .get_account(&keypairs.cpi_context_account.pubkey());
148
149                if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
150                    (tree_account, queue_account, cpi_account)
151                {
152                    for i in 0..context.test_accounts.v1_state_trees.len() {
153                        let state_mt = context.test_accounts.v1_state_trees[i].merkle_tree;
154                        let nullifier_queue_pubkey =
155                            context.test_accounts.v1_state_trees[i].nullifier_queue;
156                        let cpi_context_pubkey =
157                            context.test_accounts.v1_state_trees[i].cpi_context;
158
159                        // Update tree account with correct associated queue
160                        let mut tree_account_data = tree_acc.clone();
161                        {
162                            let merkle_tree_account = bytemuck::from_bytes_mut::<
163                                account_compression::StateMerkleTreeAccount,
164                            >(
165                                &mut tree_account_data.data_as_mut_slice()
166                                    [8..account_compression::StateMerkleTreeAccount::LEN],
167                            );
168                            merkle_tree_account.metadata.associated_queue =
169                                nullifier_queue_pubkey.into();
170                        }
171                        context.set_account(state_mt, tree_account_data);
172
173                        // Update queue account with correct associated merkle tree
174                        let mut queue_account_data = queue_acc.clone();
175                        {
176                            let queue_account = bytemuck::from_bytes_mut::<QueueAccount>(
177                                &mut queue_account_data.data_as_mut_slice()[8..QueueAccount::LEN],
178                            );
179                            queue_account.metadata.associated_merkle_tree = state_mt.into();
180                        }
181                        context.set_account(nullifier_queue_pubkey, queue_account_data);
182
183                        // Update CPI context account with correct associated merkle tree and queue
184                        let mut cpi_account_data = cpi_acc.clone();
185                        {
186                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
187                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
188                            cpi_account_data.data_as_mut_slice()
189                                [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
190                                .copy_from_slice(&state_mt.to_bytes());
191                            cpi_account_data.data_as_mut_slice()
192                                [associated_queue_offset..associated_queue_offset + 32]
193                                .copy_from_slice(&nullifier_queue_pubkey.to_bytes());
194                        }
195                        context.set_account(cpi_context_pubkey, cpi_account_data);
196                    }
197                }
198            }
199            {
200                let address_mt = context.test_accounts.v2_address_trees[0];
201                let account = context
202                    .context
203                    .get_account(&keypairs.batch_address_merkle_tree.pubkey());
204                if let Some(account) = account {
205                    context.set_account(address_mt, account);
206                }
207            }
208            // Copy batched state merkle tree accounts to devnet pubkeys
209            {
210                let tree_account = context
211                    .context
212                    .get_account(&keypairs.batched_state_merkle_tree.pubkey());
213                let queue_account = context
214                    .context
215                    .get_account(&keypairs.batched_output_queue.pubkey());
216                let cpi_account = context
217                    .context
218                    .get_account(&keypairs.batched_cpi_context.pubkey());
219
220                if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) =
221                    (tree_account, queue_account, cpi_account)
222                {
223                    use light_batched_merkle_tree::{
224                        merkle_tree::BatchedMerkleTreeAccount, queue::BatchedQueueAccount,
225                    };
226
227                    for i in 0..context.test_accounts.v2_state_trees.len() {
228                        let merkle_tree_pubkey =
229                            context.test_accounts.v2_state_trees[i].merkle_tree;
230                        let output_queue_pubkey =
231                            context.test_accounts.v2_state_trees[i].output_queue;
232                        let cpi_context_pubkey =
233                            context.test_accounts.v2_state_trees[i].cpi_context;
234
235                        // Update tree account with correct associated queue and hashed pubkey
236                        let mut tree_account_data = tree_acc.clone();
237                        {
238                            let mut tree = BatchedMerkleTreeAccount::state_from_bytes(
239                                tree_account_data.data_as_mut_slice(),
240                                &merkle_tree_pubkey.into(),
241                            )
242                            .unwrap();
243                            let metadata = tree.get_metadata_mut();
244                            metadata.metadata.associated_queue = output_queue_pubkey.into();
245                            metadata.hashed_pubkey =
246                                hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
247                        }
248                        context.set_account(merkle_tree_pubkey, tree_account_data);
249
250                        // Update queue account with correct associated merkle tree and hashed pubkeys
251                        let mut queue_account_data = queue_acc.clone();
252                        {
253                            let mut queue = BatchedQueueAccount::output_from_bytes(
254                                queue_account_data.data_as_mut_slice(),
255                            )
256                            .unwrap();
257                            let metadata = queue.get_metadata_mut();
258                            metadata.metadata.associated_merkle_tree = merkle_tree_pubkey.into();
259                            metadata.hashed_merkle_tree_pubkey =
260                                hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes());
261                            metadata.hashed_queue_pubkey =
262                                hash_to_bn254_field_size_be(&output_queue_pubkey.to_bytes());
263                        }
264                        context.set_account(output_queue_pubkey, queue_account_data);
265
266                        // Update CPI context account with correct associated merkle tree and queue
267                        let mut cpi_account_data = cpi_acc.clone();
268                        {
269                            let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer
270                            let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree
271                            cpi_account_data.data_as_mut_slice()
272                                [associated_merkle_tree_offset..associated_merkle_tree_offset + 32]
273                                .copy_from_slice(&merkle_tree_pubkey.to_bytes());
274                            cpi_account_data.data_as_mut_slice()
275                                [associated_queue_offset..associated_queue_offset + 32]
276                                .copy_from_slice(&output_queue_pubkey.to_bytes());
277                        }
278                        context.set_account(cpi_context_pubkey, cpi_account_data);
279                    }
280                }
281            }
282        }
283
284        #[cfg(not(feature = "devenv"))]
285        {
286            // Load all accounts from JSON directory
287            use crate::utils::load_accounts::load_all_accounts_from_dir;
288
289            let accounts = load_all_accounts_from_dir()?;
290
291            // Extract and verify batch_size from all V2 state tree output queues
292            // BatchedQueueMetadata layout: discriminator (8) + QueueMetadata (224) + QueueBatches.num_batches (8) + QueueBatches.batch_size (8)
293            const BATCH_SIZE_OFFSET: usize = 240;
294            let mut batch_sizes = Vec::new();
295
296            for v2_tree in &context.test_accounts.v2_state_trees {
297                if let Some(queue_account) = accounts.get(&v2_tree.output_queue) {
298                    if queue_account.data.len() >= BATCH_SIZE_OFFSET + 8 {
299                        let bytes: [u8; 8] = queue_account.data
300                            [BATCH_SIZE_OFFSET..BATCH_SIZE_OFFSET + 8]
301                            .try_into()
302                            .map_err(|_| {
303                                RpcError::CustomError("Failed to read batch_size bytes".to_string())
304                            })?;
305                        batch_sizes.push(u64::from_le_bytes(bytes) as usize);
306                    }
307                }
308            }
309
310            // Verify all batch sizes are the same
311            if !batch_sizes.is_empty() && !batch_sizes.windows(2).all(|w| w[0] == w[1]) {
312                return Err(RpcError::CustomError(format!(
313                    "Inconsistent batch_sizes found across output queues: {:?}",
314                    batch_sizes
315                )));
316            }
317
318            let batch_size = batch_sizes.first().copied().unwrap_or(0);
319
320            for (pubkey, account) in accounts {
321                context.context.set_account(pubkey, account).map_err(|e| {
322                    RpcError::CustomError(format!("Failed to set account {}: {}", pubkey, e))
323                })?;
324            }
325
326            // Initialize indexer with extracted batch size
327            let test_accounts = context.test_accounts.clone();
328            context
329                .add_indexer(&test_accounts, Some(batch_size))
330                .await?;
331        }
332
333        // reset tx counter after program setup.
334        context.transaction_counter = 0;
335
336        #[cfg(feature = "devenv")]
337        {
338            spawn_prover().await;
339        }
340        #[cfg(not(feature = "devenv"))]
341        if config.with_prover {
342            spawn_prover().await;
343        }
344
345        Ok(context)
346    }
347
348    pub fn indexer(&self) -> Result<&TestIndexer, RpcError> {
349        self.indexer.as_ref().ok_or(RpcError::IndexerNotInitialized)
350    }
351
352    pub fn indexer_mut(&mut self) -> Result<&mut TestIndexer, RpcError> {
353        self.indexer.as_mut().ok_or(RpcError::IndexerNotInitialized)
354    }
355
356    pub fn test_accounts(&self) -> &TestAccounts {
357        &self.test_accounts
358    }
359
360    /// Get account pubkeys of one state Merkle tree.
361    pub fn get_state_merkle_tree_account(&self) -> StateMerkleTreeAccounts {
362        self.test_accounts.v1_state_trees[0]
363    }
364
365    pub fn get_address_merkle_tree(&self) -> AddressMerkleTreeAccounts {
366        self.test_accounts.v1_address_trees[0]
367    }
368
369    pub async fn add_indexer(
370        &mut self,
371        test_accounts: &TestAccounts,
372        batch_size: Option<usize>,
373    ) -> Result<(), RpcError> {
374        let indexer = TestIndexer::init_from_acounts(
375            &self.payer,
376            test_accounts,
377            batch_size.unwrap_or_default(),
378        )
379        .await;
380        self.indexer = Some(indexer);
381        Ok(())
382    }
383
384    pub fn clone_indexer(&self) -> Result<TestIndexer, RpcError> {
385        Ok((*self
386            .indexer
387            .as_ref()
388            .ok_or(RpcError::IndexerNotInitialized)?)
389        .clone())
390    }
391}
392
393impl MerkleTreeExt for LightProgramTest {}
394
395impl Debug for LightProgramTest {
396    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
397        f.debug_struct("LightProgramTest")
398            .field("context", &"ProgramTestContext")
399            .field("indexer", &self.indexer)
400            .field("test_accounts", &self.test_accounts)
401            .finish()
402    }
403}